Igor
Igor

Reputation: 515

Is using unique_lock in new scope equivalent to unlock call at the end of work with shared resource?

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?

Upvotes: 5

Views: 6530

Answers (3)

Richard Hodges
Richard Hodges

Reputation: 69922

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
}

Upvotes: 2

Galik
Galik

Reputation: 48665

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
    }
}

Upvotes: 10

Jesper Juhl
Jesper Juhl

Reputation: 31488

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.

Upvotes: 8

Related Questions