Monitor Class

  • Internally synchronized class Java synchronized object

Bank Class

// Very simple "bank" class
class Bank {
public:
  void debit(const std::string &name, int amount);
  void credit(const std::string &name, int amount);
  void print(const std::string &name);
}

Naive Solution

  • Mutex as private member
  • Every member function which accesses shared data locks the mutex
class Bank {
  // Mutex to protect the data
  std::mutex mut;

  // Shared data
  ...

public:
  void credit(const std::string &name, int amount) {
    std::lock_guard lck(mut);
  }

  void credit(const std::string &name, int amount) {
    std::lock_guard lck(mut);  
  }
};

  • Member functions may need to call other member functions
    • Using multiple locks risks deadlock
  • Transactions may involve multiple member function calls
  • Results in many locking and unlocking operations
    • Race conditions
    • Potential data race
  • Existing classes need to be modified

Monitor Class

  • We write a wrapper class
    • The Bank object is a data member
    • The member functions lock a mutex and forward to the Bank object

class BankMonitor {
  std::mutex mut;
  Bank bank;

public:
  void debit(const std::string& name, int amount) {
    // Lock the mutex and forward the call
    std::lock_guard lck(mut);
    bank.debit(name, amount);
  }
};

  • Works with any type

    • Including classes that were not designed for threaded code
    • No modifications needed to the class
  • Does not help callers who want to perform transactions

    • Unnecessary locking
    • Possibility of deadlock due to multiple locking
    • Allows interruptions by other threads

Sophisticated Monitor Class

  • We make it generic

    • The wrapped class's type is the template parameter
  • Functor class with overloaded operator()

    • The argument is a callable object
    • This contains the sequence of member function calls for the transaction
    • We lock the mutex, then invoke the callable object
  • This will be a template member function

    • The type parameter is the type of the callable object
// Monitor class - can wrap any type
template <typename T>
class Monitor {
  // The object to be monitored
  T data;

  // Mutex to protect it
  std::mutex mut;

public:
  // Default / copy constructor
  Monitor<T>(T data = T{}) : data(data) {}

  // function call operator
  // argument is a callable object of type Func
  // which takes an argument of type T
  template <typename Func>
  auto operator() (Func func) {
    std::lock_guard<std::mutex> lck_guard(mut);

    // Call the function, protected by the lock
    return func(data);
  }
};

int main() {
  // Transfer $1000 from Peter to Paul
  // Must be done in a single transactiion
  
  Monitor<Bank> mon;
  
  mon([](Bank bank) {
    // Call member functions all under the same lock
    bank.debit("Peter", 1000);
    bank.credit("Paul", 1000);
    bank.print("Peter");
    bank.print("Paul");
  });
}

Advantages of Sophisticated Monitor Class

  • Works with any type

    • Including classes that were not designed for threaded code
    • No modifications needed to the class
  • Allows callers to perform transactions efficiently and safely

    • Avoids unnecessary locking
    • Avoids multiple locking
    • Prevents interruptions by other threads
  • Gives callers complete freedom

    • Which public member functions to call in a transaction
    • What order to call them in