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