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::lock is 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, and
then 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.mtx and to.mtx in:
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);
std::lock does the deadlock-free locking of from_Lock and to_Lock, and std::unique_lock does the rest (i.e. RAII stuff).
How does
std::lockwork withstd::unique_lockobjects?
Doesstd::lockcall thelock()method ofstd::unique_lock?
std::unique_lock is Lockable, and std::lock will call lock() on it, which then lock()s the mutex.
Is
std::lockresponsible for actually locking thefromandtomutexes, while thestd::unique_lockobjects merely manage the locks (i.e., release them when they go out of scope)?
std::unique_lock is 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.
What is the advantage of using
std::unique_lockwithstd::lockover directly passingstd::mutexobjects tostd::lock?
You would have to manually unlock both mutexes afterwards, and this is bug-prone. It's a similar problem as std::unique_ptr vs. new/delete. It would be fine if you immediately wrapped both mutexes in a std::lock_guard though.
For use with std::lock, you could use a simpler lock than std::unique_lock:
std::lock(from.mtx, to.mtx);
std::lock_guard<std::mutex> from_lock(from.mtx, std::adopt_lock);
std::lock_guard<std::mutex> to_lock(to.mtx, std::adopt_lock);
You only need std::unique_lock if you want to transfer ownership; otherwise you can use std::lock_guard (which is a slightly simpler type).
If you're using C++17, things get even simpler with std::scoped_lock:
// CTAD, equivalent to std::scoped_lock<std::mutex, std::mutex> lock(...)
std::scoped_lock lock(from.mtx, to.mtx);
std::scoped_lock is a replacement for std::lock_guard and has deadlock free locking built into the constructor, similar to using std::lock.
See also What's the best way to lock multiple std::mutex'es?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With