In the classic problem of transferring money from one bank account to another, the accepted solution (I believe) is to associate a mutex with each bank account, then lock both b
I believe providing each account with its own lock is fine. It provides a clear signal to any reader of your code that accessing the Account
is a critical section.
The downside to any solution involving one lock per account is that you have to be mindful of deadlock when you are writing code that manipulates multiple accounts simultaneously. But, the straightforward way to avoid that problem is to limit your interactions to be with one account at a time. This not only avoids potential deadlock problems, it also increases concurrency, since you are not blocking some other thread from being able to access some other account while the current thread is busy with something different.
Your concerns about a consistent view is valid, but can be achieved by logging the operations that are occurring with the current transaction. For example, you can decorate your deposit()
and withdraw()
operations with a transaction log.
class Account {
void deposit(const Money &amount);
void withdraw(const Money &amount);
public:
void deposit(const Money &amount, Transaction& t) {
std::lock_guard _(m_);
deposit(amount);
t.log_deposit(*this, amount);
}
void withdraw(const Money &amount, Transaction& t) {
std::lock_guard _(m_);
withdraw(amount);
t.log_withdraw(*this, amount);
}
private:
std::mutex m_;
};
Then, a transfer
is a logged withdrawal and deposit.
void transfer (Account &src, Account &dest, const Money &amount,
Transaction &t) {
t.log_transfer(src, dest, amount);
try {
src.withdraw(amount, t);
dest.deposit(amount, t);
t.log_transfer_complete(src, dest, amount);
} catch (...) {
t.log_transfer_fail(src, dest, amount);
//...
}
}
Note that the idea of a transaction log is orthogonal to how you choose to deploy your locks.