While reading on c++ std::lock, I ran into the following example from the cppreference:
void assign_lunch_partner(Employee &e1, Employee &e2)
{
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;
}
// use std::lock to acquire two locks without worrying about
// other calls to assign_lunch_partner deadlocking us
{
std::lock(e1.m, e2.m);
std::lock_guard<std::mutex> lk1(e1.m, std::adopt_lock);
std::lock_guard<std::mutex> lk2(e2.m, std::adopt_lock);
// Equivalent code (if unique_locks are needed, e.g. for condition variables)
// 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);
// Superior solution available in C++17
// std::scoped_lock lk(e1.m, e2.m);
{
std::lock_guard<std::mutex> lk(io_mutex);
std::cout << e1.id << " and " << e2.id << " got locks" << std::endl;
}
e1.lunch_partners.push_back(e2.id);
e2.lunch_partners.push_back(e1.id);
}
send_mail(e1, e2);
send_mail(e2, e1);
}
While I do understand the need for making io_mutex as static so that its status is shared among concurrent calls to assign_lunch_partner function (Please correct me if I'm wrong), but I don't understand the following:
lk object (lock_guard) was scoped? Is it because of the nature lock_guard?lk is scoped, does not this mean that the lock will be released once gone out of scope?lk (lock_guard)? At the beginning and just before updating lunch_partners vectors?If you need to acquire two locks, you might run into a deadlock if someone else tries to acquire the same locks in the reverse order. This sample shows you to use std::lock to avoid the deadlock. Immediately after locking, the mutexes are adopted by std::lock_guard objects so that they can be unlocked as soon as we leave the scope.
As mentioned, with C++17, you can do this more simply with std::scoped_lock.
You're correct in your understanding of why io_mutex is declared as static; this ensures that all concurrent calls to the function assign_lunch_partner will synchronize on the same mutex.
Now, let's answer your other questions:
Yes, it is because of the nature of std::lock_guard. std::lock_guard acquires the lock in its constructor and releases the lock in its destructor. By placing the std::lock_guard object inside a scope (enclosed by curly braces {}), the lock will be released when the scope is exited, as the destructor of std::lock_guard will be called.
Yes, exactly. Once the scope is exited, the destructor for std::lock_guard is called, and the lock is released. This is a common pattern to limit the duration of a lock to just the section of code that needs synchronization.
Why there are twice declarations of scoped lk (lock_guard)? At the beginning and just before updating lunch_partners vectors?
These two separate scoped lock guards are synchronizing different parts of the code:
lunch_partners vectors. Again, this ensures that the
console output from multiple threads is not interleaved.Essentially, these two separate scopes ensure that the messages print in a sensible and orderly manner even when this function is being called from multiple threads simultaneously. If the io_mutex lock were held for the entire duration of the function, it could potentially create a bottleneck and unnecessarily serialize parts of the code that don't need to be synchronized.
Hopefully, that clears up the usage of the scoped lock guards in this code!
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