When two threads write to the same variable without coordination, the program can produce wrong results, crash, or corrupt data. A mutex (short for "mutual exclusion") is the standard tool C++ provides to serialize access to shared state, so only one thread at a time runs the critical section. This chapter covers std::mutex and its companions, the RAII wrappers std::lock_guard and std::unique_lock, the read-heavy variant std::shared_mutex (C++17), the rarely needed std::recursive_mutex, and the timed variants. Deadlock prevention and multi-mutex locking live in the next chapter.
Before introducing a mutex, it helps to see exactly what goes wrong without one. The classic example is an inventory counter shared across worker threads. The order service starts five threads, and each one decrements the stock count for a product 1000 times. The starting stock is 5000, so the final count should be zero.
Running it again produces a different number. Maybe 824. Maybe -200. The value depends on exactly how the threads interleave, which the operating system decides.
The reason is that --stock isn't one instruction. The CPU does three things: read the current value into a register, subtract one, write the result back. If thread A reads 5000, then thread B reads 5000 before A writes back, both threads compute 4999 and both write 4999. One decrement is lost. Multiply that by millions of operations across five threads, and the final number drifts arbitrarily.
This is a data race, and the C++ standard says a program with one has undefined behavior. The "wrong number" is the friendly version of the bug. The hostile version is a crash or memory corruption that surfaces months later in production.
The fix is to make the read-modify-write happen atomically with respect to other threads. A mutex is the most general way to do that.
Now every decrement happens while the lock is held, so no two threads ever read-modify-write at the same time. The result is deterministic. The next sections walk through what std::mutex and std::lock_guard actually do.
std::mutex, declared in <mutex>, is the basic mutual exclusion primitive. It has three core operations:
| Method | What it does |
|---|---|
lock() | Blocks until the mutex is acquired by this thread. |
unlock() | Releases the mutex so another waiting thread can acquire it. |
try_lock() | Tries to acquire the mutex without blocking. Returns true on success, false if it's already locked. |
A mutex starts unlocked. When a thread calls lock(), it either acquires the mutex immediately (if no one else holds it) or blocks until the current holder calls unlock(). Only the thread that locked the mutex may unlock it; unlocking one a thread doesn't own is undefined behavior.
The same stock example using the raw lock/unlock pair, to show what the wrappers hide:
Either thread might run first, but both decrements are safe because only one thread holds stockMutex at a time.
try_lock() is useful when skipping the work is better than waiting. The order service might use it to avoid blocking the UI thread on a contended lock:
A lock() call that has to wait suspends the thread, which costs a context switch (microseconds). An uncontended lock() is much cheaper but still hits a memory barrier. Lock contention is a common reason multithreaded code doesn't scale.
Pairing lock() and unlock() by hand looks fine in tiny examples, but it falls apart in real code. Consider a slightly more realistic version of buy:
If stock < quantity, the function throws while holding the mutex. The exception propagates up the call stack, but unlock() never runs. Every subsequent call to buy blocks forever waiting for a mutex no one will release. The program deadlocks.
The same bug shows up in less obvious ways:
Or with nested logic:
Every early return, every exception, every break inside a loop is a potential way to skip unlock(). Worse, the bug is hard to spot because the function works most of the time. The symptom appears later when an unrelated function suddenly hangs because it's waiting on a lock no one will release.
The standard fix is RAII (Resource Acquisition Is Initialization): wrap the mutex in a small stack object whose destructor releases the lock. When the function returns, throws, or breaks out of a scope, the destructor runs and the mutex gets unlocked automatically. C++ ships two such wrappers in <mutex>: std::lock_guard and std::unique_lock.
std::lock_guard is the simplest RAII wrapper. Its constructor locks the mutex, its destructor unlocks it, and that's it. No other methods, no other state. It's small, fast, and the right choice in most cases.
The constructor std::lock_guard<std::mutex> lock(stockMutex); calls stockMutex.lock(). When the function exits, by return, by exception, or by falling off the end, the destructor runs and calls stockMutex.unlock(). There's no path through the function that leaves the mutex locked.
In C++17, the template arguments are deducible:
A common pattern is to limit the critical section by introducing a scope block:
Keeping the critical section small is one of the most important rules of mutex use. Holding a lock during I/O, network calls, or anything slow turns "concurrent" code into a queue of one thread.
Anything inside the locked region serializes across threads. A lock held for 10 microseconds across 100 threads creates a 1 ms queue at the back. Doing work outside the lock when possible (compute first, then write) is the single biggest concurrency tuning lever.
std::lock_guard has two restrictions worth knowing. First, it can't be unlocked early; once constructed, the lock is held until the object is destroyed. Second, it can't be moved, which means a lock_guard can't be returned from a function or stored in a container. Both restrictions are deliberate. For either capability, use std::unique_lock.
std::unique_lock is the flexible mutex wrapper. It does everything lock_guard does, and also supports unlocking and re-locking, deferred locking, transferring ownership between scopes, timed locking, and adopting a mutex that's already locked. It's slightly larger and slightly slower than lock_guard (it carries an extra flag to track whether it currently owns the lock), but the flexibility is often worth it.
The simplest use looks identical to lock_guard:
The notable features are the tag-dispatched constructors. The standard provides three tag types that change how the constructor behaves:
| Tag | Behavior |
|---|---|
std::defer_lock | Construct the wrapper without locking. Call lock() later. |
std::try_to_lock | Try to lock without blocking. Check owns_lock() to see if it worked. |
std::adopt_lock | Assume the mutex is already locked by this thread. Take ownership. |
Deferred locking is useful for constructing the lock object early but locking later, often when locking multiple mutexes:
The deadlock-safe way to lock both at once uses std::lock or std::scoped_lock.
Try-to-lock mirrors try_lock() on the mutex but wraps it in RAII so unlocking on success is automatic:
Adopt-lock is for the case where the mutex has already been locked by hand (or by some lower-level function) and RAII should take over the unlocking responsibility:
adopt_lock appears mostly with std::lock and the multi-mutex locking idiom for deadlock prevention.
Beyond the tags, unique_lock supports unlocking and re-locking as needed:
That pattern shows up in producer-consumer code and condition-variable waits. Condition variables require a std::unique_lock, not a std::lock_guard, because they need to unlock and re-lock the mutex during the wait. The condition variables chapter covers that mechanism in detail.
The cost of unique_lock versus lock_guard is small but real: an extra boolean flag and a couple of conditional checks on destruction. For tight inner-loop locks, lock_guard is the lighter choice.
| Wrapper | Locks on construction | Unlocks on destruction | Can unlock early | Movable | Use for |
|---|---|---|---|---|---|
std::lock_guard | Yes | Yes | No | No | Simple critical sections, the default |
std::unique_lock | Yes (or deferred) | Yes (if owned) | Yes | Yes | Condition variables, deferred locks, conditional locking |
Visualizing what a mutex actually does makes the rest of the chapter easier to read. A critical section is the code between lock() and unlock(), and only one thread can be inside it at a time. Every other thread that calls lock() on the same mutex blocks at the entrance until the current holder leaves.
Three threads all want to update the same shared stock. They funnel through stockMutex. Whichever one wins the race to lock() enters the critical section; the others queue up. When the winner calls unlock() (directly or by leaving the RAII scope), one waiting thread is woken and gets its turn. The OS scheduler picks the order, and for std::mutex it isn't even guaranteed to be FIFO. Don't rely on fairness.
The picture also explains why critical sections should stay small. Anything inside CS is time that every other contender spends waiting in the red box. Pre-compute as much as possible before locking, write the update inside the lock, then leave.
std::mutex treats every access the same way: one thread at a time, full stop. But many real workloads are read-heavy. A product catalog gets thousands of reads per write. A configuration object is read on every request and updated once an hour. Forcing all those reads to serialize is wasteful when none of them modify anything.
std::shared_mutex, added in C++17 and declared in <shared_mutex>, provides two locking modes:
lock(), unlock()): one writer, no readers, just like a regular mutex.lock_shared(), unlock_shared()): any number of readers, but no writer can hold it at the same time.Reads grab the shared lock and run in parallel. Writes grab the exclusive lock and block both readers and other writers.
The RAII wrappers follow the same pattern as before. Use std::unique_lock<std::shared_mutex> (or std::lock_guard) for exclusive access, and std::shared_lock<std::shared_mutex> for shared access:
All five reader threads can hold catalogMutex simultaneously when no writer is active. The writer waits until the current readers finish, then takes exclusive access, blocking any new readers until it's done.
std::shared_mutex is more expensive than std::mutex per operation, because the implementation has to track reader counts and coordinate between readers and writers. The benefit comes from parallelism: many readers progressing at once. So it pays off only when reads dominate writes and the critical sections are long enough that parallelism matters.
std::shared_mutex is slower per lock than std::mutex. It pays off only when reads dominate (think 10:1 or more) and the critical section does enough work that running readers in parallel is worth the extra overhead. For tiny critical sections with mixed reads and writes, a plain std::mutex is usually faster.
There's also a writer-starvation risk to be aware of. If readers keep arriving while a writer waits, some implementations let the readers in indefinitely, and the writer never gets a turn. Most implementations have logic to prevent this, but the standard doesn't guarantee fairness. For a steady stream of readers and rare writers, monitor write latency in production.
std::mutex has one strict rule: a thread that already owns the mutex must not call lock() again on it. Doing so is undefined behavior (in practice, the thread deadlocks on itself). That seems obvious, but it bites in code like this:
addItems locks m, then calls addItem, which also tries to lock m. The second lock() call deadlocks. The usual fix is to factor out an internal addItemUnlocked helper that assumes the caller already holds the lock:
That's the recommended approach. But if the codebase is large enough that refactoring is impractical, C++ offers std::recursive_mutex. The same thread can lock it multiple times; the mutex keeps a recursion count and is only fully released when the count drops to zero.
The cost is real: std::recursive_mutex is slower than std::mutex because every lock and unlock has to check and update the recursion counter and owner ID. The more important cost is design. Reaching for recursive_mutex often means the locking strategy isn't clean. It admits that the code can't tell, from inside a function, whether the mutex is already held. Most C++ style guides recommend recursive_mutex only when wrapping legacy code that can't be restructured.
The reader-writer variant exists too: std::recursive_mutex has no shared counterpart in the standard library. Recursive shared locking requires a custom implementation.
Sometimes blocking forever is unacceptable. A request handler might give up on a contended lock after 50 milliseconds rather than hang. std::timed_mutex (and its cousin std::recursive_timed_mutex) add two methods to the basic mutex interface:
| Method | What it does |
|---|---|
try_lock_for(duration) | Tries to lock for up to duration. Returns true if acquired, false if it timed out. |
try_lock_until(time_point) | Tries to lock until a wall-clock time point. Returns true if acquired, false if it timed out. |
The interface uses <chrono> types, so the units are explicit and type-safe:
std::unique_lock pairs with a timed mutex too, by passing the timeout as a constructor argument or by calling try_lock_for on the wrapper:
Timed mutexes are useful for keeping interactive systems responsive when a lock might be held by a slow background job. They're also slightly more expensive than plain std::mutex because the underlying implementation has to consult a clock and handle timeouts. Use them where the deadline is needed, not by default.
After the mechanics come the design questions: what should each mutex protect? Two extreme answers:
In an e-commerce service, coarse-grained might mean a single storeMutex that every order, inventory update, and cart change has to lock. Fine-grained might mean a mutex per product, a mutex per cart, a mutex per customer.
The right answer is almost always somewhere in the middle. A useful starting heuristic: one mutex per independent unit of state that can change. Independent here means changes to one unit don't logically require synchronizing with changes to another.
A worked example. A cart service tracks shopping carts for thousands of customers. Each cart is independent of the others; one customer adding a shirt has nothing to do with another customer removing a mug. A coarse mutex would serialize every cart operation in the entire store. A fine-grained mutex per cart lets each customer's operations run in parallel:
Two locks, but they protect different things. cartsMutex guards the map (so two threads don't insert into it at the same time and corrupt the structure). Each cart's own mutex guards just that cart's items. Customer 42's addItem calls don't block customer 99's, because they touch different carts.
Fine-grained locking has costs too. More mutexes mean more memory overhead, more chances of forgetting to lock something, and the deadlock risk goes up sharply when one operation needs two mutexes.
A reasonable rule: start coarse, profile, and split mutexes only where contention shows up. Premature fine-grained locking is a common way to ship a "concurrent" program that's slower than the single-threaded version and harder to debug.
Fine-grained locking adds memory (one mutex is typically 40-64 bytes on 64-bit systems) and increases the surface area for deadlocks. Don't split mutexes unless contention measurements indicate the need.
A small e-commerce program that uses three different mutex strategies side by side. An order ID generator (cheap counter, plain mutex), a customer review list (mixed reads and writes, plain mutex), and a product description cache (read-heavy, shared mutex).
Each class hides its mutex as a private member, which is good encapsulation. Callers don't need to know whether add uses a mutex or what kind. Switching the description cache to a lock-free structure later wouldn't change the public interface. The pattern, "the mutex lives next to the data it protects, and the data is only accessed through methods that lock it," is the foundation of safe concurrent class design in C++.
10 quizzes