Understanding cppreference example on lock

126 views Asked by At

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:

  1. Why the lk object (lock_guard) was scoped? Is it because of the nature lock_guard?
  2. Then if lk is scoped, does not this mean that the lock will be released once gone out of scope?
  3. Why there are twice declaration of scoped lk (lock_guard)? At the beginning and just before updating lunch_partners vectors?
2

There are 2 answers

0
Mirco Cervi On BEST ANSWER

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:

  • Why the lk object (lock_guard) was scoped? Is it because of the nature lock_guard?

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.

  • Then if lk is scoped, does not this mean that the lock will be released once gone out of scope?

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:

  1. The first one is used to synchronize the output to the console that tells you that the two employees are waiting for locks. This ensures that if multiple threads are running this function simultaneously, their outputs don't get mixed up.
  2. The second one is used to synchronize the output to the console that tells you that the two employees got the locks and are ready to update the 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!

0
Aykhan Hagverdili On

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.