lock_guard vs. unique_lock vs. scoped_lock

In the standard header <mutex>, C++ provides 3 types with “lock” in the name. All of them can be used to lock a mutex, but there are different reasons to employ each one, which this post will explain. This will not be a full examination of these things, just a few rules of thumb.

Short version

lock_guard

This should be your default choice. Use lock_guard when:

  • (Very common) It is possible to use lock_guard for your use case.
  • (Very common) You don’t want to unlock the mutex until the scope ends.

unique_lock

Use unique_lock when:

  • (Somewhat common) You need the mutex to be unlocked before the lock is destroyed. This includes times when you are awaiting a condition variable, since the wait function may need to unlock the mutex without destroying the lock.
  • (Somewhat rare) You need less common features such as creating the lock without actually locking the mutex (std::defer_lock) or try_lock_for with a timed_mutex.
  • (Very rare) You need the lock object to be created and used within a function and then returned from it.

scoped_lock

Use scoped_lock when:

  • (Rare) You need to lock more than one mutex at the same time.

Long version

lock_guard

Of the three, lock_guard has the simplest API, so there’s not much to say about it. Constructing a lock_guard locks a mutex, and destroying the lock_guard unlocks the mutex. Essentially no other operations are possible. lock_guard is acceptable for most situations, and while unique_lock and scoped_lock have their purposes, they introduce problems that lock_guard is free from. Therefore, lock_guard should be used when you have the choice.

unique_lock

When should you use unique_lock?

You should use it when you must use unique_lock. When must you use unique_lock? The main reason is that you require the lock to be unlocked before the lock is destroyed. A classic example is when you’re calling condition_variable::wait or similar:

void Thing::awaitReady() {
  std::unique_lock lock(stateMutex);
  stateCv.wait([this] { return state == State::READY; });
}

In the above example, unique_lock must be used because the mutex might need to be released and reacquired during wait . lock_guard and scoped_lock do not support unlocking and re-locking, so only unique_lock is suitable here.

When either can be used, why should lock_guard be preferred over unique_lock?

Everywhere lock_guard can be used, it’s possible to use unique_lock instead. Although unique_lock is more general, I recommend using lock_guard whenever possible. Using lock_guard results in code that’s easier to read and understand than using unique_lock, because lock_guard is simpler. In particular, instances of lock_guard are always locked as long as they exist, whereas instances of unique_lock may be locked or unlocked at various points throughout their lifetimes. In effect, encountering a unique_lock in a program forces readers to determine (or else just wonder about) when the mutex is actually held. Understanding how a program uses a unique_lock is feasible, but it demands more work and slows comprehension of the overall function even when used correctly.

void foo() {
  std::unique_lock lock(mutex);
  for (/* ... */) {
    if (/* ... */)
      /* ... */
    else
      /* ... */
  }
  if (/* ... */)
    /* ... */
  else
    /* ... */
}

As a somewhat abstract example, ask yourself whether the mutex is locked on line 9 above. The answer is maybe. It depends on what code /* ... */ stands for, some of which may call lock() or unlock(). Even if the location of all lock-manipulating calls were revealed, whether the mutex is locked on a particular line may still be unclear. Sometimes, an application simply needs to leave that question unanswered until runtime, leaving it impossible to account ahead of time for when exactly a mutex is held. But when we can write code that’s easier analyze, we should do so. That means ending the lifetime of a unique_lock as soon as possible, not just unlocking it as soon as possible. Even better than just shortening the scope is removing the question of whether and at what point unlock is called by replacing unique_lock with lock_guard.

If it were instead a lock_guard in the above function, it would be easy to see that the answer to whether it’s locked on line 9 is yes: all lock_guards are locked everywhere they exist, and determining the lifetime of a local variable is easy, as it’s just a question of scope. In this case, the lock_guard is declared in the function’s outermost scope, so it’s obviously locked until the function exits, no matter what the function’s code says or what branches are taken. Just as constants should be declared const1, it’s clearly better for readability to use a lock_guard instead of a unique_lock whenever a lock_guard suffices.

There is an additional, weaker reason to prefer lock_guard, which is that it may be implemented more cheaply. I don’t know how universal this is, but lock_guard on my machine is 8 bytes while unique_lock is 16. In theory, lock_guard might also use simpler code, though I’ve never checked.

scoped_lock

When should you use scoped_lock?

Most code that needs to lock a mutex only needs to lock one mutex. When you need to lock two or more mutexes at the same time and release them neither sooner nor later than exiting the current scope, scoped_lock may be appropriate. Like the std::lock function, scoped_lock’s constructor ensures that the mutexes passed to it will be locked in a consistent order.

void foo() {
  std::scoped_lock lock(m1, m2);
  /* critical section */
}

For example, the above code is better than making two instances of lock_guard or unique_lock.

void foo() {
  std::lock_guard l1(m1);
  std::lock_guard l2(m2);
  /* critical section */
}

void bar() {
  std::lock_guard l1(m2);
  std::lock_guard l2(m1);
  /* critical section */
}

Locking multiple mutexes poses a risk of deadlock that you must avoid by acquiring the set of locks in the same order every time. Since foo in the above code acquires m1 and then m2 while bar acquires m2 and then m1, it’s possible that one thread executing foo will hold m1 while another thread executing bar will hold m2. If that occurs, those two threads will be in a deadlock, since both threads will wait forever trying acquire the mutex held by the other thread. Using scoped_lock avoids this, regardless of the order in which you pass the mutexes to its constructor. (std::lock also avoids this, but that’s just a function for locking mutexes—it doesn’t give you an object you can destroy to conveniently unlock them.)

When either can be used, why should lock_guard be preferred over scoped_lock?

Since a scoped_lock that locks a single mutex behaves the same as a lock_guard, some2 have recommended to always use scoped_lock in place of lock_guard. I consider that a mistake and recommend the opposite: use lock_guard over scoped_lock whenever you need to lock one mutex. I do this because scoped_lock is error-prone in ways that lock_guard is not.

First, it is possible to accidentally create a scoped_lock that locks zero mutexes. I have encountered this in the wild and it has always been by mistake.

{
  std::scoped_lock lock{}; // oops: didn't provide a mutex to lock
  /* critical section */ // bug: mutex isn't held when needed
}

If you try the same mistake with a lock_guard, you’ll get a compile error instead of a buggy program:

{
  std::lock_guard lock{};
  /* critical section */
}

Given the above code, GCC says:

error: class template argument deduction failed
error: no matching function for call to 'lock_guard()'

The second mistake is that scoped_lock allows programmers to create ephemeral instances that immediately unlock whatever locks they took. Like the first pitfall, I have also seen this and it has always been an accident.

{
  std::scoped_lock (mutex); // oops: lock and then immediately unlock!
  /* critical section */ // bug: mutex isn't held when needed
}

If you try the same mistake with a lock_guard, you’ll get a compiler error instead of a buggy program:

{
  std::lock_guard (mutex);
  /* critical section */
}

Given the above code, GCC says:

error: class template argument deduction failed

scoped_lock was not in std before C++17, but lock_guard also gives us a compile error if we attempt the same misuse pre-C++17:

{
  std::lock_guard<std::mutex> (mutex);
  /* critical section */
}
error: no matching function for call to 'std::lock_guard<std::mutex>::lock_guard()'
  1. or constexpr ↩︎
  2. https://next.sonarqube.com/sonarqube/coding_rules?open=cpp%3AS5997 ↩︎

Posted

in

by

Tags: