C++ Mutexes, Concurrency, and Locks
This article has been copied from my eRCaGuy_hello_world repo here: std_mutex_vs_std_lock_guard_vs_std_unique_lock_vs_std_scoped_lock_README.md.
C++ mutexes and locks: std::mutex
, std::lock_guard
, std::unique_lock
and std::condition_variable
, and std::scoped_lock
and std::lock()
About references: both cppreference.com and cplusplus.com are community wikis. You can edit them, just like Wikipedia! Cppreference.com is generally more pedantic and up-to-date (has documentation through C++20, for instance) and difficult to understand, and cplusplus.com is generally significantly easier to understand, and more useful in that sense, but is missing most documentation after C++11.
General References:
- Concurrency support library: https://en.cppreference.com/w/cpp/thread
- Spurious wakeup: https://en.wikipedia.org/wiki/Spurious_wakeup
1. std::mutex
(C++11)
References:
- https://en.cppreference.com/w/cpp/thread/mutex
- https://cplusplus.com/reference/mutex/mutex/
A mutex is a lockable object that is designed to signal when critical sections of code need exclusive access, preventing other threads with the same protection from executing concurrently and access the same memory locations.
Features:
- A
std::mutex
allows you to prevent race conditions between multiple threads by explicitly locking and unlocking access to a shared resource.
Sample code:
#include <mutex>
std::mutex mutex;
mutex.lock();
// critical section here
mutex.unlock();
2. std::lock_guard
(C++11)
References:
- https://en.cppreference.com/w/cpp/thread/lock_guard
- https://cplusplus.com/reference/mutex/lock_guard/
A lock guard is an object that manages a mutex object by keeping it always locked.
On construction, the mutex object is locked by the calling thread, and on destruction, the mutex is unlocked. It is the simplest lock, and is specially useful as an object with automatic duration that lasts until the end of its context. In this way, it guarantees the mutex object is properly unlocked in case an exception is thrown.
Features:
- A
std::lock_guard
wraps a mutex. - At creation, it automatically locks the mutex.
- Upon destruction as it exits scope, it automatically unlocks the mutex.
- If using C++17 or later, it is recommended to use
std::scoped_lock
instead. - Even in C++11, you can use the more feature-rich
std::unique_lock
as well, to do the exact same thing if needed.
Sample code:
#include <mutex>
std::mutex mutex;
{
// `mutex.lock()` is automatically called here at the construction of the
// `std::lock_guard`
std::lock_guard<std::mutex> lock(mutex);
// critical section here
} // `mutex.unlock()` is automatically called here at the destruction of the
// `std::lock_guard`
3. std::unique_lock
(C++11)
References:
- https://en.cppreference.com/w/cpp/thread/unique_lock
- https://cplusplus.com/reference/mutex/unique_lock/
A unique lock is an object that manages a mutex object with unique ownership in both states: locked and unlocked.
On construction (or by move-assigning to it), the object acquires a mutex object, for whose locking and unlocking operations becomes responsible.
This class guarantees an unlocked status on destruction (even if not called explicitly). Therefore it is especially useful as an object with automatic duration, as it guarantees the mutex object is properly unlocked in case an exception is thrown.
Features:
- Wraps a mutex, but has more features than a
std::lock_guard
.- Unlike a
std::lock_guard
, astd::unique_lock
can be explicitly locked and unlocked after creation.
- Unlike a
- At creation, by default, it automatically locks the mutex (same as a
std::lock_guard
), but this behavior can be modified by passing special values to the constructor. - Upon destruction as it exits scope, it automatically unlocks the mutex (same as a
std::lock_guard
). - Is required by the receiving side (the side which is notified) for use by a
std::condition_variable
in order to receive the notification.- The reason a
std::unique_lock
is required by astd::condition_variable
is so that it can lock the underlying mutex each time the condition variable wakes up from a wait after a valid notification and runs a critical section of code, and unlock the underlying mutex each time A) the condition variablewait()
call spuriously wakes up and it needs to wait again, and B) upon automatic destruction when the critical section runs and is over and the scope of thestd::unique_lock
is exited.
- The reason a
- You can always use a
std::unique_lock
in place of astd::lock_guard
, but not the other way around.
Sample code:
std::unique_lock
#include <mutex>
// A global, shared mutex to be used by **all** code and threads in all examples
// below!
std::mutex mutex;
// ------------------------------------------
// Example 1: basic lock guard type usage
// ------------------------------------------
{
// `mutex.lock()` is automatically called here at the construction of the
// `std::unique_lock`
std::unique_lock<std::mutex> lock(mutex);
// critical section here
} // `mutex.unlock()` is automatically called here at the destruction of the
// `std::unique_lock`
// ------------------------------------------
// Example 2: multiple critical sections
// ------------------------------------------
{
// `mutex.lock()` is automatically called here at the construction of the
// `std::unique_lock`
std::unique_lock<std::mutex> lock(mutex);
// critical section 1 here
lock.unlock();
// do non-critical stuff here
lock.lock();
// critical section 2 here
lock.unlock();
// do non-critical stuff here
} // `mutex.unlock()` is automatically called here at the destruction of the
// `std::unique_lock`
// ------------------------------------------
// Example 3: choose to NOT automatically lock at construction
// ------------------------------------------
{
// `mutex.lock()` is NOT automatically called here at the construction of
// the `std::unique_lock`
std::unique_lock<std::mutex> lock(mutex, std::defer_lock);
// do non-critical stuff here
lock.lock();
// critical section here
lock.unlock();
// do non-critical stuff here
} // `mutex.unlock()` is automatically called here at the destruction of the
// `std::unique_lock`
std::condition_variable
The std::condition_variable
usage of the std::unique_lock
is slightly more complex:
#include <condition_variable>
#include <mutex>
// A global, shared mutex to be used by all code and threads below
std::mutex mutex;
// A global, shared condition variable to be used by all code and threads below
// in order to support one or more thread or threads to notify another thread
// or threads to trigger it or them to wake up and run.
// ie: a condition variable is a signal mechanism to get other threads to run
// when it is time for them to run.
std::condition_variable cv;
// ------------------------------------------
// Example 4: usage with `std::condition_variable`s to send a notification
// from a producer thread to a consumer thread whenever it is time to wake up
// the consumer to let it work on what the producer has provided via a shared
// memory object.
// ------------------------------------------
// Container of data to share
struct Data
{
// The producer will set this to true to indicate the data is new and has
// not been read by a consumer yet. The consumer will reset it to false
// once the data has been read.
bool isNewData = false;
int i1;
int i2;
int i3;
};
// shared data--to share between threads
Data sharedData;
// - - - - - - - - - - - - - - - - - - - - -
// Thread 1: producer
// Write data to be read by any consumer thread.
// - - - - - - - - - - - - - - - - - - - - -
{
// `mutex.lock()` is automatically called here at the construction of the
// `std::unique_lock`
std::unique_lock<std::mutex> lock(mutex);
// critical section here: have unique access via the underlying mutex to
// atomically **write to** a shared data object
sharedData.isNewData = true;
sharedData.i1 = 7;
sharedData.i2 = 8;
sharedData.i3 = 9;
// Now, immediately unlock the mutex since we are done with the critical
// section, and notify a consumer thread **which is already waiting on
// the condition variable** to get it to wake up and run.
lock.unlock();
cv.notify_one(); // wake up just one waiting, consumer thread
} // `mutex.unlock()` is automatically called here at the destruction of the
// `std::unique_lock`
// Alternative form of Thread 1:
{
mutex.lock();
sharedData.isNewData = true;
sharedData.i1 = 7;
sharedData.i2 = 8;
sharedData.i3 = 9;
mutex.unlock();
cv.notify_one(); // wake up just one waiting, consumer thread
}
// Another alternative form of Thread 1:
while (true)
{
{
// `mutex.lock()` is automatically called here at construction of
// `std::lock_guard`
std::lock_guard<std::mutex> lock(mutex);
sharedData.isNewData = true;
sharedData.i1 = 7;
sharedData.i2 = 8;
sharedData.i3 = 9;
} // `mutex.unlock()` is automatically called here at destruction of
// `std::lock_guard
cv.notify_one(); // wake up just one waiting, consumer thread
}
// - - - - - - - - - - - - - - - - - - - - -
// Thread 2: consumer
// Wake up and read data from any producer thread which has sent it a
// notification via the condition variable.
// Call `cv.wait()` WITH the boolean predicate.
// - - - - - - - - - - - - - - - - - - - - -
while (true)
{
// The `std::condition_variable::wait()` function **requires** a
// `std::unique_lock<std::mutex>&` to operate on! See:
// https://en.cppreference.com/w/cpp/thread/condition_variable/wait
std::unique_lock<std::mutex> lock(mutex); // `mutex.lock()` is automatically
// called here at construction
// Wait, meaning sleep this thread, until it is notified by the condition
// variable to wake up! The 2nd parameter passed to `wait()` is a callable
// or lambda or boolean variable "predicate" which must be `true` in order
// for this thread to stay awake and return from the `wait()` function. If
// the predicate is false, then the `wait()` function assumes it was a
// [spurious wakeup](https://en.wikipedia.org/wiki/Spurious_wakeup) and
// automatically calls the underlying `lock.unlock()` and puts the thread
// back to sleep to wait again. This wait loop goes on indefinitely until
// the predicate is true, at which point the `wait()` function will return.
// When `wait()` finally does return, the lock will have been already
// automatically taken via `lock.lock()`, which of course is just a wrapper
// around the underlying mutex, essentially calling `mutex.lock()`.
cv.wait(lock, []() {
return sharedData.isNewData;
});
// at this point, after `wait()` returns, the underlying mutex has already
// been taken via `mutex.lock()`
// critical section here: have unique access via the underlying mutex to
// atomically **read from (and/or also write to)** a shared data object
sharedData.isNewData = false; // reset our boolean "predicate" variable
// Quickly copy out the shared data to minimize time in the critical
// section. See my answer here: https://stackoverflow.com/a/71625693/4561887
Data sharedDataCopy = sharedData;
// terminate the critical section by releasing the lock
lock.unlock();
// do whatever you want to with your copy of the data now; ex: print it:
printf("sharedDataCopy: i1 = %i; i2 = %i; i3 = %i\n",
sharedDataCopy.i1, sharedDataCopy.i2, sharedDataCopy.i3);
} // `mutex.unlock()` is automatically called here at the destruction of the
// `std::unique_lock`
// Comment-free (minimal comment) form of Thread 2, for clarity:
while (true)
{
std::unique_lock<std::mutex> lock(mutex); // `mutex.lock()` is automatically
// called here at construction
cv.wait(lock, []() {
return sharedData.isNewData;
});
// `mutex.lock()` was called prior to `wait()` returning above too, even
// though the mutex was necessarily **unlocked** during the sleeping/waiting
// period
sharedData.isNewData = false; // reset our boolean "predicate" variable
Data sharedDataCopy = sharedData; // quickly copy out the shared data
lock.unlock();
printf("sharedDataCopy: i1 = %i; i2 = %i; i3 = %i\n",
sharedDataCopy.i1, sharedDataCopy.i2, sharedDataCopy.i3);
}
// Alternative form of Thread 2: call `cv.wait()` withOUT the boolean predicate,
// by using a simple `wait()` and a while loop to repeatedly check the
// predicate instead.
while (true)
{
std::unique_lock<std::mutex> lock(mutex); // `mutex.lock()` is automatically
// called here at construction
while (sharedData.isNewData == false) // OR: `while (!sharedData.isNewData)`
{
cv.wait(lock);
}
// `mutex.lock()` was called prior to `wait()` returning above too, even
// though the mutex was necessarily **unlocked** during the sleeping/waiting
// period
sharedData.isNewData = false; // reset our boolean "predicate" variable
Data sharedDataCopy = sharedData; // quickly copy out the shared data
lock.unlock();
printf("sharedDataCopy: i1 = %i; i2 = %i; i3 = %i\n",
sharedDataCopy.i1, sharedDataCopy.i2, sharedDataCopy.i3);
}
4. std::scoped_lock
(C++17) and std::lock()
(C++11)
References:
std::scoped_lock
: https://en.cppreference.com/w/cpp/thread/scoped_lockstd::lock
: https://en.cppreference.com/w/cpp/thread/lock
Features:
std::scoped_lock
is a very simple mechanism, like a C++11std::lock_guard
, except that the C++17std::scoped_lock
can be used on multiple mutexes simultaneously!- If using C++17 or later, it is recommended to use
std::scoped_lock
instead of thestd::lock_guard
. - A
std::scoped_lock
wraps one or more mutexes, just like astd::lock_guard
, except astd::lock_guard
can only wrap ONE mutex at a time! - At creation, a
std::scoped_lock
automatically locks the mutex(es). - Upon destruction as it exits scope, a
std::scoped_lock
automatically unlocks the mutex(es). - Even in C++11, you can use the more feature-rich
std::unique_lock
as well, to do the exact same thing as astd::scoped_lock
, but on a single mutex at a time. To do the same thing in C++11 on multiple mutexes at once, you must use a call to thestd::lock(mutex1, mutex2, mutexN)
function instead, thereby explicitly locking all mutexes at once. See examples below.
Sample code:
#include <mutex>
std::mutex mutex1;
std::mutex mutex2;
std::mutex mutex3;
// ------------------------------------------
// Example 1 (C++17): basic scoped lock usage on multiple mutexes at once
// ------------------------------------------
{
// `mutex.lock()` is automatically simultaneously called on all mutexes here
// at the construction of the `std::scoped_lock`
std::scoped_lock lock(mutex1, mutex2, mutex3);
// critical section here
} // `mutex.unlock()` is automatically simultaneously called on all mutexes
// here at the destruction of the `std::scoped_lock`
// ------------------------------------------
// Example 2 (C++11): equivalent to the above, but C++11-compatible. See:
// 1. https://en.cppreference.com/w/cpp/thread/scoped_lock
// 2. https://en.cppreference.com/w/cpp/thread/lock_guard/lock_guard
// 3. https://en.cppreference.com/w/cpp/thread/lock_tag
// ------------------------------------------
{
// 1. Explicitly lock all mutexes at once
std::lock(mutex1, mutex2, mutex3);
// 2. Now pass their ownership individually to `std::lock_guard` objects to
// auto-unlock them at the termination of this scope. Creating a
// `std::lock_guard` with the `std::adopt_lock` parameter "Acquires
// ownership of the mutex `m` without attempting to lock it." See:
// https://en.cppreference.com/w/cpp/thread/lock_guard/lock_guard.
std::lock_guard<std::mutex> lock1(mutex1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mutex2, std::adopt_lock);
std::lock_guard<std::mutex> lock3(mutex3, std::adopt_lock);
// critical section here
} // `mutex.unlock()` is automatically called on all mutexes
// here at the destruction of each `std::lock_guard`
// ------------------------------------------
// Example 3 (C++11): basic `std::lock()` function usage on multiple mutexes
// at once.
// ------------------------------------------
{
// Explicitly lock all mutexes at once
std::lock(mutex1, mutex2, mutex3);
// critical section here
// Unlock each mutex individually now
mutex1.unlock();
mutex2.unlock();
mutex3.unlock();
}
// ------------------------------------------
// Example 4 (C++17): basic scoped lock usage on a single mutex.
// Remember: if using C++17 or later, it is recommended to use
// `std::scoped_lock` instead of the `std::lock_guard`, even when only locking
// a single mutex.
// ------------------------------------------
{
// `mutex.lock()` is automatically called on the mutex(es) here
// at the construction of the `std::scoped_lock`
std::scoped_lock lock(mutex1);
// critical section here
} // `mutex.unlock()` is automatically called on the mutex(es)
// here at the destruction of the `std::scoped_lock`
5. std::condition_variable
(C++11)
References:
- https://en.cppreference.com/w/cpp/thread/condition_variable
- https://en.cppreference.com/w/cpp/thread/condition_variable/wait
- https://cplusplus.com/reference/condition_variable/condition_variable/
- Quote:
A condition variable is an object able to block the calling thread until notified to resume.
It uses a
unique_lock
(over amutex
) to lock the thread when one of itswait
functions is called. The thread remains blocked until woken up by another thread that calls a notification function on the samecondition_variable
object. - https://cplusplus.com/reference/condition_variable/condition_variable/wait/
At the moment of blocking the thread, the function automatically calls
lck.unlock()
, allowing other locked threads to continue.Once notified (explicitly, by some other thread), the function unblocks and calls
lck.lock()
, leavinglck
in the same state as when the function was called. Then the function returns (notice that this last mutex locking may block again the thread before returning).Generally, the function is notified to wake up by a call in another thread either to member
notify_one
or to membernotify_all
. But certain implementations may produce spurious wake-up calls without any of these functions being called. Therefore, users of this function shall ensure their condition for resumption is met.
- Quote:
Features:
- Allows one thread waking up another thread or threads to notify it or them that it’s time for it or them to run.
- The producer/notifying thread can lock the mutex like normal to write to or access the shared data.
- The consumer/
wait()
ing thread must use astd::unique_lock
when waiting to be notified and woken up. - The
std::condition_variable::wait()
function uses thestd::unique_lock
internally to unlock the mutex whenever it puts the thread to sleep to wait, and to lock the mutex whenever the thread gets woken up by a notification from thestd::condition_variable
. Here is the locking process:- The underlying mutex is already locked at the start of
std::condition_variable::wait()
. (Constructing thestd::unique_lock
automatically locked it at construction). std::condition_variable::wait()
unlocks the mutex before sleeping to wait.std::condition_variable::wait()
locks the mutex each time the thread is awakened.- If the wake up was spurious, and/or the predicate condition is not met (indicating it should wait again), it locks the mutex again before sleeping to wait again.
- When the thread is awakened and the predicate condition is
true
,std::condition_variable::wait()
finally returns, and the mutex is guaranteed to be locked at the timestd::condition_variable::wait()
returns and is complete.
- The underlying mutex is already locked at the start of
Sample code:
See above in the std::unique_lock
section!
Leave a comment
Comments are powered by Utterances. A free GitHub account is required. Comments are moderated. Be respectful. No swearing or inflammatory language. No spam.
I reserve the right to delete any inappropriate comments. All comments for all pages can be viewed and searched online here.
To edit or delete your comment: Option 1 (recommended): click the date just above your comment, ex: the
just now
or5 minutes ago
(or equivalent) part where it saysYOUR_NAME commented just now
orYOUR_NAME commented 5 minutes ago
, etc., or Option 2: click the "Comments" link at the top of the comments section below where it says how many comments have been left. Option 1 will take you directly to your comment on GitHub. Option 2 will take you to a GitHub page with all comments for this page. Then: --> find your comment on this GitHub page and click the 3 dots in the top-right of your comment --> click "Edit" or "Delete". Editing or adding a comment from the GitHub page also gives you a nicer editor.