The parent note taught you the machine : a waiting room with a doorbell, driven by a shared flag, a mutex, and wait/notify. This page throws the machine at every situation it can face — one waiter, many waiters, no waiter, a notify that arrives too early, a timed give-up, and the classic bug shapes. If you can hand-trace all of these, you have actually understood it.
Everything below builds on the parent topic and leans on std::mutex , std::unique_lock , std::lock_guard , and the Producer-Consumer Pattern .
Think of a condition_variable program as having a few independent "axes". Every real situation is one cell — a combination of choices along these axes. We must hit every cell at least once.
Axis
Cases to cover
Number of waiters
0 waiters · 1 waiter · N waiters
Which notify
notify_one · notify_all
Order of events
notify after waiter sleeps (normal) · notify before waiter sleeps (early / lost)
Predicate outcome
pred true on first check · pred false → sleep · woken but still false (spurious/stolen)
Timing
wait forever · wait_for timeout (deadline passes vs. arrives in time)
Degenerate / bug
flag set without lock (lost wakeup) · if instead of while (stolen item) · notify with no waiter (dropped)
The nine worked examples below are each tagged [Cell: …] so you can see the matrix fill up.
[Cell: 1 waiter · pred true on first check]
Shared: bool ready = true; (already set before the worker runs). The worker calls cv.wait(lk, []{ return ready; }). How many times does the thread go to sleep? What prints?
Forecast: Guess now — does the thread sleep zero times, once, or block forever?
Worker locks the mutex. Why this step? wait requires you to already hold the std::unique_lock ; the lock protects the read of ready.
wait(lk, pred) first evaluates pred(). Why? The predicate overload is defined as while(!pred()) cv.wait(lk); — it checks before sleeping . Here pred() returns true.
!true == false, so the while body never runs. Why this matters? The thread never touches the doorbell and never sleeps. wait returns immediately, still holding the lock.
Worker prints started!.
Verify: Sleep count = 0 . Because a notify sent before any waiter is lost (a cv has no memory), the correctness here came entirely from the flag , not the notification — exactly the parent's rule. Output: one line started!.
[Cell: 1 waiter · pred false → sleep · notify_one · notify after sleep]
bool ready = false;. Worker waits; 100 ms later main does { lock; ready = true; } cv.notify_one();. Trace the sleeps.
Forecast: How many times does pred() get evaluated in total?
Worker locks, evaluates pred() → false. Why? First check of the internal while. Evaluation #1.
!false == true → enters loop → calls cv.wait(lk). Why? Single-arg wait atomically unlocks the mutex and sleeps. What it looks like: the worker leaves the "active" lane and enters the waiting room (figure s02, blue arrow down).
main locks, sets ready = true, unlocks, notify_one(). Why set under the lock? To close the lost-wakeup window (see Example 7).
Doorbell rings → worker wakes → wait re-locks the mutex → loop re-evaluates pred() → true. Evaluation #2. Why re-check? Could be spurious; the loop guarantees we only proceed when the world is really ready.
!true == false → loop exits → started! prints.
Verify: pred() evaluated exactly 2 times, thread slept exactly 1 time. Total notifies delivered to a waiting thread: 1.
[Cell: 1 waiter · woken but pred still false]
Same setup as Example 2, but the OS spuriously wakes the worker once before main sets ready. How many pred() evaluations now?
Forecast: With if instead of while, what would go wrong?
Worker: pred() → false (eval #1) → sleeps.
Spurious wake: OS wakes the thread with nobody having notified. wait re-locks, loop re-checks pred() → still false (eval #2). Why we survive: the while sends the thread back to sleep . With an if, the thread would fall through and print started! while ready is still false — a bug.
Later main sets ready = true, notify_one().
Worker wakes, re-locks, pred() → true (eval #3), exits, prints.
Verify: pred() evaluated 3 times; thread slept 2 times. Same visible output (started! once), but the loop absorbed the extra wake safely.
[Cell: N waiters · notify_one · one item · stolen wakeup]
3 consumers all wait on a queue. Producer pushes one item and calls notify_one(). Consumers each do cv.wait(lk, []{ return !q.empty(); }) then pop one item. What happens to the 2 consumers that don't get it?
Forecast: Does notify_one wake exactly the one that will succeed?
All 3 consumers evaluate pred() → empty → sleep. 3 threads in the waiting room.
Producer pushes 1 item, notify_one(). Why one? Only one item exists, so waking one worker is enough. One consumer (say C1) wakes.
C1 re-locks, pred() → !empty == true, pops the item → queue empty again. C1 proceeds.
C2, C3 remain asleep (they were never notified). Why fine? No item for them, so leaving them asleep saves CPU — this is why notify_one is correct when you add exactly one unit of work.
Verify: Items consumed = 1 . Waiters still sleeping = 2 . Sleeps that turned into wasted wakeups = 0 (because we chose notify_one, not notify_all).
[Cell: N waiters · notify_all · one item · stolen wakeup]
Same 3 consumers, but the producer (perhaps at shutdown) calls notify_all() after pushing one item. Now all 3 wake and race for the single item.
Forecast: Without the loop, how many consumers would try to pop from an empty queue?
3 consumers asleep. Producer pushes 1 item, notify_all() → all 3 wake and contend for the mutex.
C1 wins the lock , pred() → !empty, pops item, queue empty. C1 proceeds.
C1 releases lock. C2 grabs it , re-evaluates pred() → empty == true, so !pred() → back to sleep . Why re-check? C1 stole the item C2 was woken for. The while catches this; an if would let C2 call q.front() on an empty queue → undefined behaviour.
C3 likewise re-checks → empty → sleeps.
Verify: Items consumed = 1 . Consumers that woke = 3 , of which 2 re-slept via the loop. Bug avoided precisely because of while(!pred()).
[Cell: wait_for · deadline passes · deadline met]
cv.wait_for(lk, 200ms, []{ return ready; }) returns a bool: the final value of the predicate . Consider two runs.
Run A (times out): nobody sets ready within 200 ms.
Run B (in time): main sets ready and notifies at 120 ms.
Forecast: What boolean does each run return?
Run A: worker sleeps; at 200 ms the timer fires. wait_for re-locks, evaluates pred() → false → returns false. Why return the predicate, not "did we time out"? So your code branches on truth of the condition , robust against spurious wakes near the deadline.
Run B: at 120 ms the notify arrives. Worker wakes, pred() → true → returns true before the 200 ms deadline.
Verify: Run A returns false (and 200 ≥ 200 ms elapsed). Run B returns true (and elapsed ≈ 120 < 200 ms). Numerically: false and true; elapsed times 200 and 120 .
[Cell: flag set without lock · notify_one · notify before sleep]
Buggy code: main does ready = true; without locking, then cv.notify_one(); — and it happens to run in the tiny gap after the worker checked pred() but before it called wait(lk).
Forecast: Does the worker ever wake up?
Worker locks, pred() → false. Not yet sleeping.
CPU switches to main: ready = true (no lock), notify_one(). But no thread is inside wait yet → the notify hits an empty waiting room → dropped (cv has no memory).
CPU returns to worker: it calls cv.wait(lk) and sleeps — forever , because the only notify already vanished.
Verify: Number of notifications delivered to a waiting thread = 0 ; worker sleep is permanent (a hang). Fix: set ready = true under the same mutex the worker uses. Then step 2 cannot interleave between the worker's check and its sleep, because both are inside the critical section — the notify lands on a properly parked waiter. This is a Race Condition ; the un-woken thread is effectively in a one-sided Deadlock .
[Cell: real-world · N producers/consumers · notify_all]
A kitchen has a counter that holds at most 3 pizzas. 2 cooks (producers) add pizzas; 4 waiters (consumers) take them. Two conditions are needed: "counter not full" (cooks wait) and "counter not empty" (waiters wait). Use two predicates on one mutex. If a cook adds a pizza while the counter was at 0, how many waiters may wake?
Forecast: Do you use notify_one or notify_all for the "not empty" event?
Cook locks, wait(lk, []{ return count < 3; }). Why? Don't overflow the counter. Sleeps if full.
Cook adds pizza → count = 1, unlocks, cv_notEmpty.notify_all() (or notify_one if one item = one waiter). Why notify_all is safe: each woken waiter re-checks count > 0 in a while; extras re-sleep (Example 5 logic). With exactly one pizza added, notify_one is the efficient choice.
A waiter wakes, pred() → count > 0, takes a pizza → count = 0, then cv_notFull.notify_one() to tell a possibly-waiting cook there's room.
Verify: With counter cap 3 : after adding one pizza to an empty counter, exactly 1 pizza is available; using notify_one wakes 1 waiter who succeeds; using notify_all wakes all 4 , of whom 1 succeeds and 3 re-sleep. Both leave count = 0 after service.
[Cell: exam-style counting]
One consumer waits on a queue. A producer pushes 4 items, one at a time, each followed by notify_one(). The consumer drains all currently available items in an inner while(!q.empty()) per wake. Assume no spurious wakes and that each notify arrives while the consumer is asleep, before it pushes the next. How many times does the outer pred() get evaluated, and how many times does the consumer sleep?
Forecast: Is it 4 sleeps, or fewer?
Consumer: outer pred() → empty → sleep (eval #1, sleep #1).
Push item, notify. Consumer wakes, pred() → true (eval #2), drains 1 item, loops back, pred() → empty → sleep (eval #3, sleep #2).
Repeat for items 2, 3, 4. Each item adds 2 evals (one true that drains, one false that re-sleeps) — except the last, which re-checks done.
Count: initial eval #1, then per item a "true" eval and a "false→sleep" eval.
Verify: For K = 4 items delivered one-at-a-time-while-asleep: outer pred() evaluations = 2 K + 1 = 9 ; consumer sleeps = K = 4 (initial sleep, then re-sleep after each of the first 3 drains — the 4th drain either sleeps again or exits on done; counting the initial + one per delivered-then-empty gap gives 4).
With notify_all, one item, and 3 waiters, how many consumers proceed and how many re-sleep? 1 proceeds; 2 re-sleep because the while re-checks the (now false) predicate.
What does cv.wait_for return? The final value of the predicate — true if the condition holds, false if it timed out with the condition still false.
In the lost-wakeup bug, why is the notify dropped? It is sent while no thread is inside wait; a condition_variable has no memory, so a notify with no waiter is lost.
If the predicate is already true when wait(lk,pred) is called, how many times does the thread sleep? Zero — the predicate is checked before sleeping, so wait returns immediately.
Why use notify_one (not notify_all) after pushing exactly one item? Only one unit of work exists, so waking one waiter is enough; waking all just makes extras wake and re-sleep, wasting CPU.
Recall One-line summary of the whole matrix
Correctness always comes from flag + mutex + while-loop ; notify is only a nudge, and every failure case (spurious, stolen, lost, timeout) is handled by re-checking the predicate under the lock.