Mutexes Locks
By the end of this lesson you'll be able to protect shared data from data races with std::mutex , pick the right RAII lock, dodge deadlocks, coordinate threads with a condition variable, and count without locks using std::atomic — the core toolkit of safe C++ concurrency.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free C++ course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
A mutex is the talking stick in a meeting: only the person holding it may speak, and everyone else waits until it's passed on. The shared data is the conversation; the mutex makes sure two people don't talk over each other and garble it. A deadlock is two people each refusing to hand over their stick until they get the other's — so nobody ever speaks again. The whole lesson is really one idea: hold the stick only while you must, and agree on who picks up which stick first.
1. The Data Race and std::mutex
When two threads change the same variable at once, you get a data race : the result is wrong and unpredictable. That's because counter++ is really three steps — read, add one, write back — and the threads' steps interleave. A mutex (mutual exclusion) fixes this: a thread locks it before touching the shared data and unlocks when done, so only one thread is in that critical section at a time. Always wrap the lock in a RAII guard so it unlocks even if an exception is thrown.
Your turn. The program below counts across two threads but the increment isn't protected yet. Add the one missing line marked ___ using the hint, then run it.
2. lock_guard vs unique_lock vs scoped_lock
All three are RAII locks — they unlock automatically when they go out of scope, so you never forget. They differ in flexibility. lock_guard is the simplest: lock once, auto-unlock, no manual control. unique_lock adds power — you can unlock and relock, defer locking, or hand it to a condition variable. scoped_lock (C++17) locks several mutexes at once, deadlock-free. Reach for the simplest one that does the job.
lock_guard — your default. One mutex, one scope, zero ceremony. Use it for the vast majority of critical sections.
unique_lock — when you need more: defer_lock to lock later, early unlock() , or (most importantly) to pass to a condition_variable , which requires a unique_lock because it must release and re-acquire the mutex while waiting.
scoped_lock — whenever you hold two or more mutexes at once. It locks them with a deadlock-free algorithm, so you don't have to reason about ordering yourself.
3. Deadlock — and How to Avoid It
A deadlock is when two threads each hold a lock the other one needs, so both wait forever and your program freezes. The classic cause is inconsistent lock ordering : one thread locks A then B, another locks B then A. The cures are simple: always lock mutexes in the same order everywhere , or lock them together atomically with std::lock (then adopt them) or, cleaner still, with std::scoped_lock .
4. std::condition_variable (Producer/Consumer)
A mutex stops threads clashing, but how does one thread wait for another to produce work without burning the CPU in a busy loop? A condition variable lets a thread sleep until it's notified. The consumer calls cv.wait(lock, predicate) : this releases the mutex, sleeps, and re-locks only when the predicate is true. The producer calls cv.notify_one() to wake it. The predicate also guards against spurious wake-ups — rare false alarms where wait returns for no reason.
5. std::atomic — Lock-Free Counters
For a single shared value like a counter or flag, a full mutex is overkill. std::atomic<int> makes each operation — increment, compare, exchange — a single indivisible step the hardware guarantees, with no lock at all. It's faster and simpler than a mutex for these small cases . (When you must update several variables together, or a whole container, you still need a mutex — one atomic step isn't enough.)
Your turn again. Turn the plain counter below into a lock-free one with std::atomic . Fill in the two blanks:
6. std::recursive_mutex (Brief)
A plain std::mutex deadlocks if the same thread tries to lock it twice. A std::recursive_mutex keeps a count, so one thread may lock it several times (and must unlock the same number of times) — handy when a locked function calls another locked function. Treat it as a last resort: needing it is usually a sign the design should be restructured so the lock is taken in exactly one place.
No blanks this time — just a brief and an outline. Build a shared balance that three threads deposit into safely, run it, and check your output against the expected total in the comments. This is exactly the pattern real concurrent code is built from.
Practice quiz
Why does counter++ across two unsynchronised threads give a wrong, random total?
- The compiler reorders the loop
- Integers overflow
- counter++ is read-modify-write, and the threads' steps interleave (a data race)
- One thread always wins and the other does nothing
Answer: counter++ is read-modify-write, and the threads' steps interleave (a data race). counter++ is three steps (read, add, write); without a mutex the threads interleave those steps and lose updates.
What is the simplest RAII lock for one mutex and one scope?
- std::lock_guard
- std::unique_lock
- std::scoped_lock
- std::recursive_mutex
Answer: std::lock_guard. lock_guard locks on construction and unlocks at end of scope with no manual control — the default for a simple critical section.
Which lock can you manually unlock and relock, and hand to a condition_variable?
- std::lock_guard
- std::scoped_lock
- std::mutex itself
- std::unique_lock
Answer: std::unique_lock. unique_lock is flexible: defer locking, early unlock/relock, and it is what condition_variable::wait requires.
Which C++17 lock takes several mutexes at once with a deadlock-free algorithm?
- std::lock_guard
- std::scoped_lock
- std::recursive_mutex
- std::timed_mutex
Answer: std::scoped_lock. scoped_lock locks multiple mutexes together using std::lock's ordering, so you needn't reason about order yourself.
What is the classic cause of a deadlock?
- Two threads locking the same mutexes in opposite order
- Using a lock_guard instead of unique_lock
- Locking a mutex only once
- Calling notify_one too early
Answer: Two threads locking the same mutexes in opposite order. Inconsistent lock ordering — thread 1 locks A then B while thread 2 locks B then A — leaves each waiting on the other forever.
When using std::lock(mtxA, mtxB) to grab both atomically, how do the guards adopt them?
- lock_guard<mutex> a(mtxA, defer_lock);
- lock_guard<mutex> a(mtxA, try_lock);
- lock_guard<mutex> a(mtxA, adopt_lock);
- lock_guard<mutex> a(mtxA);
Answer: lock_guard<mutex> a(mtxA, adopt_lock);. After std::lock grabs both, you wrap each with adopt_lock so the guard takes ownership of an already-locked mutex.
Why does condition_variable::wait need a unique_lock rather than a lock_guard?
- lock_guard is too slow
- wait must release the mutex while sleeping and re-acquire it on wake
- unique_lock is thread-safe and lock_guard is not
- It does not — lock_guard works too
Answer: wait must release the mutex while sleeping and re-acquire it on wake. wait unlocks the mutex while the thread sleeps and relocks on wake, which only unique_lock supports.
What is the role of the predicate (lambda) in cv.wait(lock, pred)?
- It chooses which thread to wake
- It sets the timeout
- It locks a second mutex
- It guards against spurious wake-ups by re-checking the condition
Answer: It guards against spurious wake-ups by re-checking the condition. The predicate makes wait return only when the condition is genuinely true, protecting against false-alarm wake-ups.
When is std::atomic<int> preferable to a mutex?
- When protecting a whole std::queue
- For a single shared value like a counter or flag, where each op is indivisible
- When several variables must change together
- Never — atomics are always slower
Answer: For a single shared value like a counter or flag, where each op is indivisible. atomic suits single values (counter/flag) with indivisible ops; multi-variable or container updates still need a mutex.
What does std::recursive_mutex allow that a plain std::mutex does not?
- Locking from multiple processes
- Lock-free atomic increments
- The same thread to lock it multiple times (and unlock the same number)
- Automatic deadlock detection
Answer: The same thread to lock it multiple times (and unlock the same number). A plain mutex self-deadlocks if one thread locks it twice; recursive_mutex counts locks so re-locking by the same thread is allowed. It is usually a design smell.