std::scoped_lock behaviour with a single mutex
Asked Answered
P

3

5

Context:

I know that std::lock_guard became kind of deprecated since the arrival of with std::scoped_lock.

I also know that std::scoped_lock is preferred since it can handle several mutexes and uses a deadlock avoidance algorithm the same way as std::lock does.

I'm interested here in the case we have only one single mutex and thus we don't need to care about deadlock avoidance.

I have read from this answer that:

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.

Question:

I'm wondering how much this sentence is true.

I mean, is it guaranteed (by the standard) that with a single mutex, std::scoped_lock will be specialized so that it will get rid of any unnecessary overhead due to deadlock avoidance handling ?


My thoughts:

After some investigation on the question, I found from cppreference the following sentence:

If several mutexes are given, deadlock avoidance algorithm is used as if by std::lock.

Which could let us deduce that such a thing would not happen otherwise (i.e. if only one mutex is given).
But once again, it is just an assumption.

From this c++ draft I don't see any explicit mention about such a specialization.
The only sentence I got is:

When sizeof...(MutexTypes) is 1, the supplied Mutex type shall meet the Cpp17BasicLockable requirements. Otherwise, each of the mutex types shall meet the Cpp17Lockable requirements.

(emphasis mine)

I know that the BasicLockable requirements mandate the existence of lock() and unlock() functions which meet the conditions such as defined here.
On the other hand, the Lockable requirements assume the BasicLockable requirements with the addition of a try_lock() function which meet the conditions such as defined there.

I know that the try_lock() function is required in order to run the deadlock avoidance algorithm used by std::lock.

From what's stated in the above draft extract, the try_lock() function is thus not required if we give only one mutex to std::scoped_lock.
Is this sufficient to deduce/consider that the above specialization is always defined (and presumably behaves as std::lock_guard would do).
I would say yes but as I never saw any explicit mention about it, I wonder if I'm right or if I missed something ?


EDIT:

I just noticed that I missed the most important part here which states:

Effects: Initializes pm with tie(m...). Then if sizeof...(MutexTypes) is 0, no effects. Otherwise if sizeof...(MutexTypes) is 1, then m.lock(). Otherwise, lock(m...).

(emphasis mine)

Which answers my interrogations, std::lock is called only when there is more than one given mutex. I should have seen it before asking the question...

Pyelitis answered 29/1, 2020 at 12:11 Comment(0)
C
2

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.

Cherlynchernow answered 29/1, 2020 at 12:31 Comment(1)
Yes, I have noticed this part quite too late (mentioned in my edit).Pyelitis
H
4

std::scoped_lock is required to behave identically to std::lock_guard when only one mutex is supplied. Hence the different requirement for the single mutex case.

This could be done with a specialization, or it could be done via a different internal mechanism, as long as the behaviour is the same.

Hawkes answered 29/1, 2020 at 12:24 Comment(1)
Thanks for your confirmation. Actually I missed the most important part. I should have noticed it before asking my question :)Pyelitis
C
2

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.

Cherlynchernow answered 29/1, 2020 at 12:31 Comment(1)
Yes, I have noticed this part quite too late (mentioned in my edit).Pyelitis
D
0

The standard will seldomly guarantee some kind of optimization though specialization (Notable examples are specializations on different iterator typea and the abomination which is std::vector<bool>). For this there are two ways to go at it:

  1. Trust your compiler/standard library implementation. Compilers are epic, they do extremely advanced kinds of optimization, some of them you can only dream of. The implementations of the STL are in most cases fantastic. There are areas where they are slower since they have to be able to handle weird edge cases, but here there has to be a different specialization already since only BasicLockable is required for the one argument case, so it will have an implementation that does not need try_lock, so why shouldn't it be efficient.
  2. Perform your code. Test if it is fast enough, test if scoped_lock is on a hot path of your code and if you really think (and have data to prove it) that scoped_lock is slow, then and only then replace it with lock_guard and test again.
Deformity answered 29/1, 2020 at 12:32 Comment(1)
Agreed, but I cannot rely on possible compiler optimizations to issue a general compiler-independent certainty. The only guarantees I can trust are the ones given by the standard. The underlying question was that I wanted to know if I could completely get rid of std::lock_guard with the guarantee that it will not incur any unnecessary overhead with only one mutex.Pyelitis

© 2022 - 2024 — McMap. All rights reserved.