Std::Lock_Guard or Std::Scoped_Lock

std::lock_guard or std::scoped_lock?

Late answer, and mostly in response to:

You can consider std::lock_guard deprecated.

For the common case that one needs to lock exactly one mutex, std::lock_guard has an API that is a little safer to use than scoped_lock.

For example:

{
std::scoped_lock lock; // protect this block
...
}

The above snippet is likely an accidental run-time error because it compiles and then does absolutely nothing. The coder probably meant:

{
std::scoped_lock lock{mut}; // protect this block
...
}

Now it locks/unlocks mut.

If lock_guard was used in the two examples above instead, the first example is a compile-time error instead of a run-time error, and the second example has identical functionality as the version which uses scoped_lock.

So my advice is to use the simplest tool for the job:

  1. lock_guard if you need to lock exactly 1 mutex for an entire scope.

  2. scoped_lock if you need to lock a number of mutexes that is not exactly 1.

  3. unique_lock if you need to unlock within the scope of the block (which includes use with a condition_variable).

This advice does not imply that scoped_lock should be redesigned to not accept 0 mutexes. There exist valid use cases where it is desirable for scoped_lock to accept variadic template parameter packs which may be empty. And the empty case should not lock anything.

And that's why lock_guard isn't deprecated. scoped_lock and unique_lock may be a superset of functionality of lock_guard, but that fact is a double-edged sword. Sometimes it is just as important what a type won't do (default construct in this case).

std::scoped_lock or std::unique_lock or std::lock_guard?

The two objects are for different purposes. scoped_lock is for the simple case of wanting to lock some number of mutex objects in a deadlock-free way. Locking a single mutex is just a special case of locking multiple ones. The object is completely immobile, and it's very simple.

unique_lock provides a number of features, few of which are especially applicable when simultaneously locking multiple mutexes.

  • Deferred locking. Deferring would have to be all or nothing; you either defer locking all the mutexes or none of them. It's not clear why you would want to defer locking a series of mutexes, since you would have to relinquish any locks that succeeded if any of them failed.

  • Timeout locks. If you want a timeout of 100ms, does that mean that locking all of the mutexes should take no more than 100ms? That is, if the first 3 lock immediately, but the next one takes 75ms, should it be considered a timeout if the fifth takes 30ms?

  • Adoption of mutexes. The whole point of locking multiple mutexes in a single operation is to be able to avoid deadlocks. This is done by locking the mutexes in an order that is globally consistent. That is, any place where you lock those mutex objects with std::lock equivalent calls will lock them in the same order, no matter what.

    If one of the mutexes has already been locked (and thus the lock should be adopted), then it was locked outside of std::lock, and thus you have no guarantee that it was locked in the globally consistent order. And that ignores the difficulty of specifying which mutexes to adopt and which ones to lock.

  • Transfer of ownership (being moveable). This is a dubious prospect for multiple mutexes for similar reasons as adopting locks. The guarantees against deadlocks only work if a single call to std::lock or equivalent locks all of the mutexes of interest. If you're moving ownership of these scoped_locks around, it becomes very easy to be at a point in code where you have multiple scoped_locks in the same scope, when you could have locked all of them in one go. This courts the very kind of deadlock that std::lock was created to avoid.

Note that std::lock (the basis of scoped_lock's functionality) doesn't even try to provide any of these features.

Could there be a specialization of scoped_lock which took only one mutex type that offered the behavior of unique_lock? Sure. But that would violate the purpose of scoped_lock, which was to be a deadlock-safe locker for multiple mutexes. It only obsoleted lock_guard by accident, since it had the identical interface in the case of a single mutex.

Besides, having template specializations with vastly different interfaces and capabilities doesn't usually work out well. See vector<bool> as an example.

Should I use lock_guard, scoped_lock or unique_lock in this situation?

To summarize what was already written in comments:

Yes, the code is correct. However, it may be inefficient, because it disallows reading from any array element while writing to another array element. You might want to do more fine-grained synchronization by using a mutex for each array element.

class Blah 
{
const int numDevices = getDevices();
std::vector<std::mutex> mutexes;
std::vector<StateInfo> stateInfo;
public:

Blah() : mutexes(numDevices), stateInfo(numDevices){}

void writers(StateInfo &newSi, const int i)
{
std::lock_guard<std::mutex> guard(mutexes[i]);
stateInfo[i] = newSi;
}

StateInfo reader(const int i)
{
std::lock_guard<std::mutex> guard(mutexes[i]);
return stateInfo[i];
}
};

Why c++11 std::lock and std::scoped_lock need at least 2 parameters?


It doesn't require two, it can lock one or more.

From the cppreference page you took your example from (emphasis mine):

The class scoped_lock is a mutex wrapper that provides a convenient RAII-style mechanism for owning one or more mutexes for the duration of a scoped block.

std::scoped_lock is a convenience utility for acquiring multiple mutexes - it will use deadlock avoiding mechanism under the hood. In C++11 and C++14 we only had std::lock(), but it is not a RAII mechanism (it will not unlock mutexes automatically).

You can also use std::scoped_lock with single mutex, then it becomes equivalent to std::lock_guard

Why put std::lock before std::lock_guard


Why do we apply the std::lock and then apply 2 std::lock_guards with std::adopt_lock instead of just applying 2 std::lock_guards one after another??

If you used two std::lock_guard without std::lock the order of locking for swap(a, b); would be the opposite of swap(b, a);, where a and b are Xs. If one thread tried swap(a, b); while another tried swap(b, a); they could deadlock. The first thread would own the lock on a's mutex and wait for b's while the second thread would own the lock on b's mutex and wait for a's. Using std::lock ensures that the locking order is always consistent.

Why cant we just put this 2 std::mutexes in the std::scoped_lock??

If you look at the date of publication for the article you linked, c++17 did not exist yet. Since std::scoped_lock was introduced by c++17, it could not have been used in the article. This kind of locking problem is what std::scoped_lock is design to solve and should be used in modern code.

std::scoped_lock behaviour with a single mutex

If you read the specification of lock_guard (which is right above scoped_lock) it should be clear.

[thread.lock.guard]-3

Initializes pm with m. Calls m.lock()

[thread.lock.scoped]-3

Initializes pm with tie(m...). [...] Otherwise if sizeof...(MutexTypes) is 1, then m.lock(). [...]

It doesn't explicitly mention to use lock_guard but it is required to have the same behavior.

scoped_lock inside lock_guard, is it redundant?

They lock different mutexes. Whether this makes sense depends on what is do something. For example it could be:

void B(){
boost::mutex::scoped_lock b_lock(b_mutex);
/* do something that needs b_mutex locked */
}

void A(){
const std::lock_guard<std::mutex> a_lock(a_mutex);
/* do something that needs a_mutex locked */
B();
}

It seems like A could be changed to

void A(){
{
const std::lock_guard<std::mutex> a_lock(a_mutex);
/* do something that needs a_mutex locked */
}
B();
}

But whether this is still correct depends on details that were left out from the posted code.

Locking two different mutexes is not redundant, because other threads may lock only one of them.

std::scoped_lock and mutex ordering

The order for std::lock isn't defined until run-time, and it is not fixed. It is discovered experimentally by the algorithm for each individual call to std::lock. The second call to std::lock could lock the mutexes in a different order than the first, even though both calls might use the same list of mutexes in the same order at the call site.

Here is a detailed performance analysis of several possible implementations of std::lock: http://howardhinnant.github.io/dining_philosophers.html

Using a fixed ordering of the mutexes is one of the algorithms that is performance-compared in the above link. It is not the best performing algorithm for the experiments conducted.

The libstdc++ implementation the OP points to is a high quality implementation of what the analysis labels "Smart & Polite"

std::unique_lock std::mutex or std::lock_guard std::mutex ?

The difference is that you can lock and unlock a std::unique_lock. std::lock_guard will be locked only once on construction and unlocked on destruction.

So for use case B you definitely need a std::unique_lock for the condition variable. In case A it depends whether you need to relock the guard.

std::unique_lock has other features that allow it to e.g.: be constructed without locking the mutex immediately but to build the RAII wrapper (see here).

std::lock_guard also provides a convenient RAII wrapper, but cannot lock multiple mutexes safely. It can be used when you need a wrapper for a limited scope, e.g.: a member function:

class MyClass{
std::mutex my_mutex;
void member_foo() {
std::lock_guard<mutex_type> lock(this->my_mutex);
/*
block of code which needs mutual exclusion (e.g. open the same
file in multiple threads).
*/

//mutex is automatically released when lock goes out of scope
}
};

To clarify a question by chmike, by default std::lock_guard and std::unique_lock are the same.
So in the above case, you could replace std::lock_guard with std::unique_lock. However, std::unique_lock might have a tad more overhead.

Note that these days (since, C++17) one should use std::scoped_lock instead of std::lock_guard.



Related Topics



Leave a reply



Submit