4

I have seen a lot of examples of code when developer uses std::unique_lock in new scope for automatically unlocking mutex:

...
// do some staff
{
  std::unique_lock<std::mutex> lock(shared_resource_mutex);
  // do some actions with shared resource
}
// do some staff
...

In my opinion it would be better to implement this behaviour using method unlock from std::unique_lock API in this way:

...
// do some actions
std::unique_lock<std::mutex> lock(shared_resource_mutex);
// do some actions with shared resource
lock.unlock();
// do some actions
...

Are these two fragments of code equivalent? For what purpose developers use the first variant? Maybe to emphasize (using parentheses) code that can not be executed parallel?

πάντα ῥεῖ
  • 1
  • 13
  • 116
  • 190
Igor
  • 477
  • 5
  • 13

3 Answers3

8

When the object is destroyed at the end of the scope, the lock is released. That's the whole point of RAII.

The good thing about using RAII is that you cannot forget to unlock and it doesn't matter how you leave the scope. If an exception is thrown for example, the lock will still be released.

If all you need is lock at construction and unlock at destruction, then std::scoped_lock is an even simpler/more appropriate class to use though.

Jesper Juhl
  • 30,449
  • 3
  • 47
  • 70
  • _"If an exception is thrown for example, the lock will still be released."_ But that would be true for the other variant as well. – πάντα ῥεῖ Aug 04 '18 at 07:13
  • Yep, I know about RAII, but I'm talking about another thing. std::unique_lock has unlock method to release lock on resource early (when possible) to improve parallelization, even the exception is thrown before unlock - everything will be ok. And asking about the thing why people use { } for this action instead of unlock call. – Igor Aug 04 '18 at 07:17
  • Yes, but in that case you'd have two different ways in which the lock is released rather than just one. As it is now, a beginner might not see where the mutex is released. With also calling unlock, a beginner might assume that the lock won't be released on a throw. – Cubic Aug 04 '18 at 07:20
8

I would say the former method is safer, more consistent and easier to read.

First consider safety:

void function()
{
    std::unique_lock<std::shared_mutex> lock(mtx);

    // exclusive lock stuff

    lock.unlock();

//  std::shared_lock<std::shared_mutex> lock(mtx); // whoops name in use
    std::shared_lock<std::shared_mutex> lock2(mtx);

    // read only shared lock stuff here

    lock2.unlock(); // what if I forget to do this?

    lock.lock(); // if I forgot to call lock2.unlock() undefined behavior

    // back to the exclusive stuff

    lock.unlock();  
}

If you have different locks to acquire/release and you forget to call unlock() then you may end up trying to lock the same mutex twice from the same thread.

That is undefined behavior so it may go unnoticed but cause trouble.

And what if you call either lock() or unlock() on the wrong lock variable.... (say on lock2 rather than lock1?) the possibilities are frightening.

Consistency:

Also, not all lock types have a .unlock() function (std::scoped_lock, std::lock_guard) so it is good to be consistent with your coding style.

Easier to read:

It is also easier to see what code sections use locks which makes reasoning on the code simpler:

void function()
{
    {
        std::unique_lock<std::shared_mutex> lock(mtx);

        // exclusive lock stuff
    }

    {
        std::shared_lock<std::shared_mutex> lock(mtx);

        // read only shared lock stuff here
    }

    {
        std::unique_lock<std::shared_mutex> lock(mtx);

        // back to the exclusive stuff
    }
}
Galik
  • 47,303
  • 4
  • 80
  • 117
2

Both of your approaches are correct, and you might choose either of them depending on circumstance. For example, when using a condition_variable/lock combination it's often useful to be able to explicitly lock and unlock the lock.

Here's another approach that I find to be both expressive and safe:

#include <mutex>

template<class Mutex, class Function>
decltype(auto) with_lock(Mutex& m, Function&& f)
{
    std::lock_guard<Mutex> lock(m);
    return f();
}

std::mutex shared_resource_mutex;

void something()
{
    with_lock(shared_resource_mutex, [&]
    {
        // some actions
    });

    // some other actions
}
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142