std::mutex, lock_guard, unique_lock, scoped_lock, condition_variable
C++ mutual exclusion primitives — mutex variants, RAII lock guards, condition variables, and correct patterns for thread-safe code.
std::mutexsince C++11A non-recursive, non-transferable mutual exclusion primitive that serializes concurrent access to shared state; always acquired and released through RAII wrappers, never by raw lock()/unlock() calls.
Overview
<mutex> (C++11) defines the mutex family, RAII wrappers, and deadlock-avoidance utilities. <shared_mutex> (C++14/17) adds reader-writer variants. The cost of acquiring an uncontended mutex is typically 20–100 ns; std::atomic operations run 1–5 ns. For isolated counters and flags, prefer std::atomic — reach for mutex only when you need to protect a multi-step invariant.
#include <mutex>
std::mutex mtx;
int counter = 0;
void increment() {
std::lock_guard lock(mtx); // C++17: CTAD; C++11: lock_guard<std::mutex>
++counter;
}Mutex variants
| Type | Recursive? | Timed? | Shared lock? | Since |
|---|---|---|---|---|
std::mutex | ❌ | ❌ | ❌ | C++11 |
std::recursive_mutex | ✅ | ❌ | ❌ | C++11 |
std::timed_mutex | ❌ | ✅ | ❌ | C++11 |
std::recursive_timed_mutex | ✅ | ✅ | ❌ | C++11 |
std::shared_timed_mutex | ❌ | ✅ | ✅ | C++14 |
std::shared_mutex | ❌ | ❌ | ✅ | C++17 |
Prefer std::shared_mutex (C++17) over std::shared_timed_mutex (C++14) when timeout-based acquisition is unnecessary — the leaner interface permits tighter implementations.
std::recursive_mutex is a design smell. It exists for callback-heavy APIs where re-entrant locking is unavoidable; in new code, restructure first.
RAII lock wrappers
lock_guard — simplest, fixed scope (C++11)
Non-copyable, non-movable. Prefer it for the common case: one mutex, one scope.
void append(std::vector<int>& v, int x) {
std::lock_guard<std::mutex> guard(mtx); // C++11
v.push_back(x);
}
// C++17: CTAD eliminates the template argument
std::lock_guard guard(mtx);scoped_lock — multiple mutexes, deadlock-free (C++17)
Acquires an arbitrary number of mutexes atomically using a deadlock-avoidance algorithm. Supersedes the std::lock + adopt_lock dance from C++11 for all multi-mutex scenarios.
std::mutex m1, m2;
void safe_swap(Resource& a, Resource& b) {
std::scoped_lock lock(a.mtx, b.mtx); // C++17 — order-independent
std::swap(a.data, b.data);
}
// C++11 equivalent — error-prone but sometimes necessary
void safe_swap_11(Resource& a, Resource& b) {
std::lock(a.mtx, b.mtx);
std::lock_guard<std::mutex> la(a.mtx, std::adopt_lock);
std::lock_guard<std::mutex> lb(b.mtx, std::adopt_lock);
std::swap(a.data, b.data);
}unique_lock — movable, unlockable mid-scope (C++11)
Required by condition_variable::wait. Stores an owns_lock boolean flag, making it marginally heavier than lock_guard. Supports deferred, try, and timed acquisition.
void process(Cache& cache) {
std::unique_lock lock(mtx);
auto snapshot = cache.snapshot(); // fast copy under lock
lock.unlock();
auto result = compute(snapshot); // slow work outside lock
lock.lock();
cache.commit(result);
}
// Deferred: construct unlocked, lock when ready
std::unique_lock lock(mtx, std::defer_lock);
// ... other state setup ...
lock.lock();
// Try-lock: non-blocking
std::unique_lock trylock(mtx, std::try_to_lock);
if (trylock.owns_lock()) { /* acquired */ }
// Timed: C++11, requires timed_mutex
std::unique_lock timedlock(timed_mtx, std::chrono::milliseconds(50));
if (timedlock.owns_lock()) { /* acquired within deadline */ }shared_lock — shared (read) lock (C++14)
Used with shared_mutex to allow concurrent reads while serializing writes.
#include <shared_mutex>
class Config {
mutable std::shared_mutex mtx_; // C++17
std::unordered_map<std::string, std::string> data_;
public:
std::string get(const std::string& key) const {
std::shared_lock lock(mtx_); // C++14 — multiple readers concurrent
auto it = data_.find(key);
return it != data_.end() ? it->second : "";
}
void set(const std::string& key, std::string val) {
std::unique_lock lock(mtx_); // exclusive write
data_.insert_or_assign(key, std::move(val));
}
};Reader-writer locks only pay off when reads are frequent and significantly longer than lock acquisition, and write contention is low. Benchmark before adopting — a plain std::mutex frequently wins due to smaller code size and better cache behavior.
condition_variable
std::condition_variable (C++11) enables threads to block until a condition becomes true. It requires unique_lock<mutex>. Always supply a predicate — spurious wakeups are guaranteed by the standard to occur.
#include <condition_variable>
#include <queue>
template<typename T>
class BlockingQueue {
std::mutex mtx_;
std::condition_variable cv_;
std::queue<T> q_;
bool closed_ = false;
public:
void push(T val) {
{
std::lock_guard lock(mtx_);
q_.push(std::move(val));
}
cv_.notify_one();
}
void close() {
{
std::lock_guard lock(mtx_);
closed_ = true;
}
cv_.notify_all();
}
bool pop(T& out) {
std::unique_lock lock(mtx_);
cv_.wait(lock, [&] { return !q_.empty() || closed_; });
if (q_.empty()) return false;
out = std::move(q_.front());
q_.pop();
return true;
}
};cv.wait(lock, pred) is strictly equivalent to while (!pred()) cv.wait(lock) — the predicate is rechecked after every wakeup.
std::condition_variable_any (C++11) accepts any BasicLockable, not just unique_lock<mutex>. In C++20 it gains overloads that accept a std::stop_token for cooperative cancellation:
// C++20: interruptible wait
std::condition_variable_any cv;
std::shared_mutex smtx;
// Returns early if stop_token fires; check stop_token.stop_requested() on return
cv.wait(std::shared_lock{smtx}, stop_token, predicate);call_once and once_flag
std::call_once (C++11) executes an initializer exactly once even under concurrent invocation, with lower overhead than a checked mutex.
#include <mutex>
class HeavyResource {
std::once_flag flag_;
std::unique_ptr<Backend> backend_;
public:
Backend& get() {
std::call_once(flag_, [&] {
backend_ = std::make_unique<Backend>("production");
});
return *backend_;
}
};For static-duration singletons, C++11 guarantees thread-safe initialization of function-local statics — no once_flag needed:
Logger& logger() {
static Logger instance; // C++11: initialization is thread-safe
return instance;
}Common Pitfalls
Unnamed temporary — destroyed immediately
// BUG: anonymous temporary; destroyed at the semicolon — lock held for zero time
std::lock_guard<std::mutex>(mtx);
// Fix: name it
std::lock_guard lock(mtx);Condition variable without predicate
cv.wait(lock); // WRONG: spurious wakeup exits even if condition is false
cv.wait(lock, [&] { return !queue_.empty() || done_; }); // CORRECTProtecting only half a transfer
struct Account {
std::mutex mtx;
int balance;
void transfer_to(Account& dst, int amount) {
// BAD: dst.mtx unheld; concurrent reverse transfer is a data race
std::lock_guard lock(mtx);
balance -= amount;
dst.balance += amount;
// GOOD: both mutexes, deadlock-free
std::scoped_lock both(mtx, dst.mtx); // C++17
balance -= amount;
dst.balance += amount;
}
};Holding the lock across slow operations
Copy shared data under the lock; do the slow work outside it.
// BAD: holds lock during blocking network call
{
std::lock_guard lock(mtx);
send_over_network(shared_data_); // may block for seconds
}
// GOOD: narrow critical section
Data snapshot;
{
std::lock_guard lock(mtx);
snapshot = shared_data_;
}
send_over_network(snapshot);Deadlock prevention
- Consistent acquisition order — always lock
m1beforem2across all code paths. std::scoped_lock(C++17) — acquire multiple locks in one statement; deadlock impossible.- Lock hierarchies — assign numeric levels; a thread holding level N may only acquire level < N next.
- Avoid nested locks — one mutex at a time unless
scoped_lockcovers all of them.
See Also
std::atomic— lock-free arithmetic on scalars and pointers; prefer over mutex for simple counters and flagsstd::jthread— C++20 joinable thread with cooperative cancellation viastop_tokenstd::latch,std::barrier— C++20 one-shot and cyclic thread synchronization pointsstd::semaphore— C++20 counting semaphore for resource-count–bounded access