What's the difference between first locking and creating a lock_guard(adopt_lock) and creating a unique_lock(defer_lock) and locking?
1) First code sample
{
static std::mutex io_mutex;
std::lock_guard<std::mutex> lk(io_mutex);
std::cout << e1.id << " and " << e2.id << " are waiting for locks" << std::endl;
}
This is a standard lock guard, when the scope is exited, the lock lk
is released
{
std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
std::lock(lk1, lk2);
std::cout << e1.id << " and " << e2.id << " got locks" << std::endl;
// ...
}
Here we first create the locks without acquiring them (that's the point of std::defer_lock
) and then, using std::lock
on both locks simultaneously makes sure that they are acquired without the risk of a deadlock if another caller of the function interleaves (we could have a deadlock if you replaced it with two successive calls to std::lock
:
{
std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
std::lock(lk1);
std::lock(lk2); // Risk of deadlock !
std::cout << e1.id << " and " << e2.id << " got locks" << std::endl;
// ...
}
2) Second code sample
void swap(X& lhs, X&rhs){
if(&lhs == &rhs)
return;
// m is the std::mutex field
std::lock(lhs.m, rhs.m);
std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);
std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);
swap(lhs.some_detail, rhs.some_detail);
}
Now, here we first acquire the locks (still avoiding deadlocks), and then we create the lockguards to make sure that they are properly released.
Note that std::adopt_lock
requires that the current thread owns the mutex (which is the case since we just locked them)
Conclusion
There are 2 patterns here :
1) Lock both mutex at the same time, then create the guards
2) Create the guards, then lock both mutex at the same time
Both patterns are equivalent, and aim at the same thing : safely lock two mutex at the same time, and ensure that unlocking always occur for both.
As for the difference between std::lock_guard
and std::unique_lock
, you should see this other SO post, most of the time std::lock_guard
is enough.
There's actually a paragraph (3.2.6) in the book explaining that the code is virtually equivalent and you could replace one with the other. The only difference being is that std::unique_lock
tends to take more space and is a fraction slower than std::lock_guard
.
Bottom line is whenever you don't need the additional flexibility that std::unique_lock
provides, go with std::lock_guard
.
The difference is robustness against future changes. In the adopt_lock
version there is a window where the mutexes are locked but not owned by a cleanup handler:
std::lock(lhs.m, rhs.m);
// <-- Bad news if someone adds junk here that can throw.
std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);
std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);
It's also possible to accidentally remove/omit one of the guard declarations without any compile-time errors. The problem will be obvious at runtime when the deadlock hits, but it's not fun to trace a deadlock back to its source.
The defer_lock
version doesn't suffer from either of these problems. Since the guard objects are declared before the locking happens, there's no unsafe window. And of course if you omit/remove one of the guard declarations you'll get a compiler error at the std::lock
call.