I'm working with a piece of multithreading code that involves bank account transfers. The goal is to safely transfer money between accounts without running into race conditions. I'm using std::mutex to protect the bank account balances during transfers:
My question centers around the use of std::unique_lock with std::lock. Instead of passing the std::mutex objects directly to std::lock, I'm wrapping them with std::unique_lock and passing those to std::lock.
How does std::lock work with std::unique_lock objects?
Is std::lock responsible for actually locking the from and to mutexes, while the std::unique_lock objects merely manage the locks (i.e., release them when they go out of scope)?
Does std::lock call the lock() method of std::unique_lock?
What is the advantage of using std::unique_lock with std::lock over directly passing std::mutex objects to std::lock?
struct bank_account
{
bank_account(int balance) :
mtx(), balance{ balance }
{}
std::mutex mtx;
int balance;
};
void transfer(bank_account& from, bank_account& to, int amount)
{
std::unique_lock<std::mutex> from_Lock(from.mtx, std::defer_lock);
std::unique_lock<std::mutex> to_Lock(to.mtx, std::defer_lock);
std::lock(from_Lock, to_Lock);
if (amount <= from.balance)
{
std::cout << "Before: " << amount << " from: " << from.balance << " to: " << to.balance << '\n';
from.balance -= amount;
to.balance += amount;
std::cout << "After: " << amount << " from: " << from.balance << " to: " << to.balance << '\n';
}
else
{
std::cout << amount << " is greater than " << from.balance << '\n';
}
}
int main()
{
bank_account A(200);
bank_account B(100);
std::vector<std::jthread> workers;
workers.reserve(20);
for (int i = 0; i < 10; ++i)
{
workers.emplace_back(transfer, std::ref(A), std::ref(B), 20);
workers.emplace_back(transfer, std::ref(B), std::ref(A), 10);
}
}
The purpose of
std::lockis to provide a deadlock free locking (see libc++ implementation) of multiple Lockable objects. The classic problem is that if you have two locks L1 and L2, andthen there may be a deadlock because each thread could hold one lock and require the other from another thread. This issue applies when you're locking
from.mtxandto.mtxin:std::lockdoes the deadlock-free locking offrom_Lockandto_Lock, andstd::unique_lockdoes the rest (i.e. RAII stuff).Q&A
std::unique_lockis Lockable, andstd::lockwill calllock()on it, which thenlock()s the mutex.std::unique_lockis perfectly capable of doing locking and unlocking a mutex on its own. The only thing it can't do is implement a deadlock free locking when multiple locks are involved.You would have to manually unlock both mutexes afterwards, and this is bug-prone. It's a similar problem as
std::unique_ptrvs.new/delete. It would be fine if you immediately wrapped both mutexes in astd::lock_guardthough.Further Improvements
For use with
std::lock, you could use a simpler lock thanstd::unique_lock:You only need
std::unique_lockif you want to transfer ownership; otherwise you can usestd::lock_guard(which is a slightly simpler type).If you're using C++17, things get even simpler with
std::scoped_lock:std::scoped_lockis a replacement forstd::lock_guardand has deadlock free locking built into the constructor, similar to usingstd::lock.See also What's the best way to lock multiple std::mutex'es?