310

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?

Stephan Dollberg
  • 32,985
  • 16
  • 81
  • 107

4 Answers4

280

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).

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • 1
    "There exist valid use cases where it is desirable for scoped_lock to accept variadic template parameter packs which may be empty". What are those cases? – Martin Dec 21 '21 at 14:11
  • 2
    The author of `scoped_lock` Mike Spertus showed me such a case in the hall between meetings a couple of years ago. I'm afraid I don't remember the details well enough to replicate it. And after a quick search I don't find an example online nor in one of Mike's papers. If you desperately need an example I recommend contacting Mike directly. – Howard Hinnant Dec 21 '21 at 14:28
  • hi Howard, what if one needs to signal a condition variable within a multiple mutex transaction? I'm thinking that the right approach would be to capture N-1 locks first with `scoped_lock`, then the last with `unique_lock` , retry them all if the last one fails, or do the transaction and signal the condition with the unlockable one – lurscher Dec 23 '21 at 01:47
  • It is hard to know for sure without more details. But my first guess at the best tools would be a `condition_variable_any` waiting on a `scoped_lock`. – Howard Hinnant Dec 23 '21 at 03:42
  • 1
    A note about the scoped_lock "typo" bug: today I learned that GCC has a warning flag called "-Wparentheses" that can detect that kind of typo. The warning is enabled with "-Wall". [docs](https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html) – starball May 15 '22 at 06:40
215

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).

Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
  • 14
    Also, thanks to class template argument deduction, you don't even have to list out the lockable types. – Nicol Bolas Mar 25 '17 at 17:39
  • 3
    @NicolBolas: That's true, but that also applies to `lock_guard`. But it certainly makes the guard classes a bit easier to use. – Kerrek SB Mar 25 '17 at 17:42
  • 11
    scoped_lock is C++17 only – Shital Shah May 19 '18 at 00:13
  • 3
    As it is c++17, compatibility is a particularly good reason for its existance. I also vehemently disagree with any absolutist claim of "you should only ever use" when the ink is still drying from this standard. – Paul Childs Oct 31 '18 at 04:39
  • 31
    [Howard Hinnant's answer](https://stackoverflow.com/a/60172828/2323908) explains why `scoped_lock` is NOT a strictly superior version of `lock_guard`. The reason for keeping `lock_guard` is not _solely_ due to compatibility! – Joakim Thorén Dec 15 '20 at 08:50
  • 1
    @JoakimThorén: Thanks, that's a good point. I don't think this was considered during the design of `scoped_lock`, which started out as a proposal to extend `lock_guard`, but had to be renamed because of ABI breaking concerns. But that's a good insight. – Kerrek SB Dec 17 '20 at 01:44
  • 1
    If it's like this, why wasn't it marked as deprecated? – HAL9000 Oct 06 '21 at 12:01
153

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

Drew Dormann
  • 59,987
  • 13
  • 123
  • 180
Stephan Dollberg
  • 32,985
  • 16
  • 81
  • 107
  • 11
    Answered your own question after only 10 min. Did you really not know? – Walter Mar 25 '17 at 18:04
  • 37
    @Walter I did https://stackoverflow.blog/2011/07/01/its-ok-to-ask-and-answer-your-own-questions/ – Stephan Dollberg Mar 25 '17 at 18:05
  • @Ruslan not sure what you mean. There was no 10 minutes delay. When you create a question you can already provide the answer. – Stephan Dollberg Mar 25 '17 at 20:02
  • Ah, right, I believed the comment by Walter without checking times. – Ruslan Mar 25 '17 at 20:03
  • 1
    It will be interesting to see if `scoped_lock lk;` turns out to be a bug or a feature. – Howard Hinnant Mar 25 '17 at 20:32
  • @HowardHinnant good point, I think libc++ has an assert that checks for empty template list? https://github.com/llvm-mirror/libcxx/blob/master/include/mutex#L512 -- nvm, missed the empy specialization. What's that even supposed to do? – Stephan Dollberg Mar 25 '17 at 20:38
  • 5
    When I brought it up in committee, the answer was "nothing." It may be that the degenerate case of some algorithm, this is exactly the right thing. Or it may be that enough people accidentally lock nothing when they intended to lock something is a common problem. I'm really not sure. – Howard Hinnant Mar 25 '17 at 20:44
  • 3
    @HowardHinnant: `scoped_lock lk; // locks all mutexes in scope`. LGTM. – Kerrek SB Mar 25 '17 at 22:06
  • 2
    @KerrekSB: `scoped_lock lk;` is the new shorthand for `scoped_lock<> lk;`. There **are** no mutexes. So you're right. ;-) – Howard Hinnant Mar 25 '17 at 22:12
  • 2
    @inf Not sure where they got the "...as long as you pretend you're on Jeopardy" bit from as it would be in the opposite order. – Paul Childs Oct 31 '18 at 04:35
  • 1
    Note that you don't need to provide template arguments for `scoped_lock`. It is available since C++17, where template arguments can be deduced for classes. – Daniel Langr Nov 21 '19 at 10:52
23

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!

jonspaceharper
  • 4,207
  • 2
  • 22
  • 42
Chen Li
  • 4,824
  • 3
  • 28
  • 55