When should std::scoped_lock be used over std::shared_lock?
I don't think I understand the use of std::scoped_lock.
Or, is std::scoped_lock just a variant of std::unique_lock for holding multiple mutexes?
Ref: "std::lock_guard or std::scoped_lock?"
Related
I just learned about std::lock_guard and I was wondering why it is a template.
Until now I have only seen std::lock_guard<std::mutex> with std::mutex inside the angle brackets.
Using std::lock_guard<std::mutex> is indeed quite common.
But you can use std::lock_guard with other mutex types:
Various standard mutex types, e.g.: std::recursive_mutex.
Your own mutex type. You can use any type, as long as it is a BasicLockable, i.e. it supports the required methods: lock(), unlock().
C++17 introduced a new lock class called std::scoped_lock.
Judging from the documentation it looks similar to the already existing std::lock_guard class.
What's the difference and when should I use it?
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:
lock_guard if you need to lock exactly 1 mutex for an entire scope.
scoped_lock if you need to lock a number of mutexes that is not exactly 1.
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).
The scoped_lock is a strictly superior version of lock_guard that locks an arbitrary number of mutexes all at once (using the same deadlock-avoidance algorithm as std::lock). In new code, you should only ever use scoped_lock.
The only reason lock_guard still exists is for compatibility. It could not just be deleted, because it is used in current code. Moreover, it proved undesirable to change its definition (from unary to variadic), because that is also an observable, and hence breaking, change (but for somewhat technical reasons).
The single and important difference is that std::scoped_lock has a variadic constructor taking more than one mutex. This allows to lock multiple mutexes in a deadlock avoiding way as if std::lock were used.
{
// safely locked as if using std::lock
std::scoped_lock<std::mutex, std::mutex> lock(mutex1, mutex2);
}
Previously you had to do a little dance to lock multiple mutexes in a safe way using std::lock as explained this answer.
The addition of scope lock makes this easier to use and avoids the related errors. You can consider std::lock_guard deprecated. The single argument case of std::scoped_lock can be implemented as a specialization and such you don't have to fear about possible performance issues.
GCC 7 already has support for std::scoped_lock which can be seen here.
For more information you might want to read the standard paper
Here is a sample and quote from C++ Concurrency in Action:
friend void swap(X& lhs, X& rhs)
{
if (&lhs == & rhs)
return;
std::lock(lhs.m, rhs.m);
std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);
std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);
swap(lhs.some_detail, rhs.some_detail);
}
vs.
friend void swap(X& lhs, X& rhs)
{
if (&lhs == &rhs)
return;
std::scoped_lock guard(lhs.m, rhs.m);
swap(lhs.some_detail, rhs.some_detail);
}
The existence of std::scoped_lock means that most of the cases where you would have used std::lock prior to c++17 can now be written using std::scoped_lock, with less potential for mistakes, which can only be a good thing!
C++17 introduced both std::shared_mutex and std::scoped_lock. My problem is now, that it seems, that scoped_lock will lock a shared mutex always in exclusive (writer) mode, when it is passed as an argument, and not in shared (reader) mode. In my app, I need to update an object dst with data from an object src. I want to lock src shared and dst exclusive. Unfortunately, this has the potential for deadlock, if a call to another update method with src and dst switched occurs at the same time. So I would like to use the fancy deadlock avoidance mechanisms of std::scoped_lock.
I could use scoped_lock to lock both src and dst in exclusive mode, but that unnecessarily strict lock has performance backdraws elsewhere. However, it seems, that it is possible to wrap src's shared_mutex into a std::shared_lock and use that with the scoped_lock: When the scoped_lock during its locking action calls try_lock() on the shared_lock, the later will actually call try_shared_lock() on src's shared_mutex, and that's what I need.
So my code looks as simple as this:
struct data {
mutable std::shared_mutex mutex;
// actual data follows
};
void update(const data& src, data& dst)
{
std::shared_lock slock(src.mutex, std::defer_lock);
std::scoped_lock lockall(slock, dst.mutex);
// now can safely update dst with src???
}
Is it safe to use a (shared) lock guard like this inside another (deadlock avoidance) lock guard?
As pointed out by various commentators, who have read the implementation code of the C++ standard library: Yes, the use of a std::shared_mutex wrapped inside a std::shared_lock() as one of the arguments to std::scoped_lock() is safe.
Basically, a std::shared_lock forwards all calls to lock() to lock_shared() on the mutex.
std::shared_lock::lock -----------> mutex()->lock_shared(). // same for try_lock etc..
Another possible solution
std::shared_lock lk1(src.mutex, std::defer_lock);
std::unique_lock lk2(dst.mutex, std::defer_lock);
std::lock(lk1, lk2);
std::lock is a function that accepts any number of Lockable objects and locks all of them (or aborts with an exception, in which case they will all be unlocked).
std::scoped_lock according to cppreference is a wrapper for std::lock, with the added functionaliy of calling unlock() on each Lockable object in its destructor. That added functionality is not required here, as std::shared_lock lk1 and std::unique_lock lk2 also work as lock guards, that unlock their mutexes, when they go out of scope.
Edit: various clarifications
Mutex: Add thread safety to shared resources
Lock: Add RAII (and possibly extra functionality) to mutex
Different locks let you lock the mutex in different ways:
unique_lock: exclusive access to resource (for write)
shared_lock: shared access to resource (for simultaneous reads)
scoped_lock: same as unique_lock, but with less features
scoped_lock is a bare bone exclusive lock that is locked when constructed and unlocked when destroyed. unique_lock and shared_lock are exclusive and shared locks respectively that are also locked and unlocked with their default constructor and destructor. However, they also provide extra functionality. For example, you can try to luck them, or you can unlock them before they are destroyed.
So a typical use case would be to use a shared_lock for shared access (when multiple threads read the same resource), and use a unique_lock or a scoped_lock for exclusive access (depending on if you need the extra features of unique_lock or not).
When reading the documentation about std::scoped_lock and std::lock_guard, it seams that the only difference is that scoped_lock can handle multiple lock guard and can avoid deadlock when unlocking.
Is this the only difference? If I have only one mutex, should I therefore keep using use lock_guard?
As far as I know the only important difference is that the scoped_lock has a variadic constructor taking more than one mutex as you mentioned. In addition you can implement a single-argument version of scoped_lock with template specialization.
So the lock_guard is kinda "deprecated" non-formally.
I think lock_guard still exists because of compatibility.
Can you combine std::recursive_mutex with std::condition_variable, meaning do something like this:
std::unique_lock<std::recursive_mutex> lock(some_recursive_mutex)
some_condition_var.wait(lock);
If it's not allowed, then why not?
I am using VC++11.
You can, if you use std::condition_variable_any, which allows for any type of object that supports the Lockable concept.
However, in the case of recursive mutex, you do have to ensure that the given thread has only locked the recursive mutex once, since the condition variable only will use the unlock method on the unique_lock once during the wait.
You can do that with a std::condition_variable_any which can take any kind of lockable but plain std::condition_variable is specialized for std::unique_lock<std::mutex>.