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::lock
work withstd::unique_lock
objects?
Doesstd::lock
call 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::lock
responsible for actually locking thefrom
andto
mutexes, while thestd::unique_lock
objects 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_lock
withstd::lock
over directly passingstd::mutex
objects 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?