Home > Articles > Programming > C/C++

  • Print
  • + Share This
From the author of

Improving External Locking

Now let's assume that BankAccount doesn't use its own locking at all, and has only a thread-neutral implementation:

class BankAccount {
  int balance_;
public:
  void Deposit(int amount) {
    balance_ += amount;
  }
  void Withdraw(int amount) {
    balance_ -= amount;
  }
};

Now you can use BankAccount in single-threaded and multi-threaded applications alike, but you need to provide your own synchronization in the latter case.

Say we have an AccountManager class that holds and manipulates a BankAccount object:

class AccountManager {
  Mutex mtx_;
  BankAccount checkingAcct_;
  BankAccount savingsAcct_;
  ...
};

Let's also assume that, by design, AccountManager must stay locked while accessing its BankAccount members. The question is, how can we express this design constraint using the C++ type system? How can we state "You have access to this BankAccount object only after locking its parent AccountManager object"?

The solution is to use a little bridge template ExternallyLocked that controls access to a BankAccount.

template <class T, class Owner>
class ExternallyLocked {
  T obj_;
public:
  ExternallyLocked() {}
  ExternallyLocked(const T& obj) : obj_(obj) {}
  T& Get(StrictLock<Owner>&) { return obj_; }
  void Set(const T& obj, Lock<Owner>&) { obj_ = obj; }
};

ExternallyLocked cloaks an object of type T, and actually provides full access to that object through the Get and Set member functions, provided you pass a reference to a Lock<Owner> object.

Instead of making checkingAcct_ and savingsAcct_ of type BankAccount, AccountManager holds objects of type ExternallyLocked<BankAccount, AccountManager>:

class AccountManager {
  Mutex mtx_;
  ExternallyLocked<BankAccount, AccountManager> checkingAcct_;
  ExternallyLocked<BankAccount, AccountManager> savingsAcct_;
  ...
};

The pattern is the same as before—to access the BankAccount object cloaked by checkingAcct_, you need to call Get. To call Get, you need to pass it a Lock<AccountManager>. The one thing you have to take care of is to not hold pointers or references you obtained by calling Get. If you do that, make sure that you don't use them after the Lock has been destroyed. That is, if you alias the cloaked objects, you're back from "the compiler takes care of that" mode to "you must pay attention" mode.

Typically, you use ExternallyLocked as shown below. Suppose you want to execute an atomic transfer from your checking account to your savings account:

void AccountManager::Checking2Savings(int amount) {
  Lock<AccountManager> guard(*this);
  checkingAcct_.Get(guard).Withdraw(amount);
  savingsAcct_.Get(guard).Deposit(amount);
}

We achieved two important goals. First, the declaration of checkingAcct_ and savingsAcct_ makes it clear to the code reader that that variable is protected by a lock on an AccountManager. Second, the design makes it impossible to manipulate the two accounts without actually locking a BankAccount. ExternallyLocked is what could be called active documentation.

  • + Share This
  • 🔖 Save To Your Account