Question bank — std - condition_variable
Before you start, keep three plain-English facts on a sticky note, because almost every trap reduces to one of them:
Recall The three facts every trap leans on
Fact 1 — no memory: a notify sent while nobody is waiting simply vanishes. The condition_variable does not queue it up for later. Fact 2 — the flag carries the truth: correctness comes from the shared predicate (a boolean over shared state), not from the notification. The notify is only a "wake up and re-check" nudge. Fact 3 — the same mutex guards both the check and the change: the waiter's predicate-check and the notifier's state-change must be serialized by the same mutex, or a wakeup can slip through the crack.
Picture the machinery first
Before the traps, ground your eyes on four pictures. Every trap below is just one of these diagrams gone wrong — when you get stuck on a question, come back and point at the arrow that breaks.
Figure 1 — the correct handshake. This is a swim-lane timeline: the Consumer lane on the left, the Producer lane on the right, and time flows downward. Notice the three atomic steps inside wait: it releases the mutex (violet arrow leaving the lane), sleeps (grey bar), and re-acquires the lock (violet arrow returning) only after the notify. The predicate is checked twice — once before sleeping, once after waking.
Figure 2 — the lost-wakeup race. Same two lanes, but the producer flips the flag and notifies without holding the lock, landing in the tiny window (red band) after the consumer read false but before it slept. The notify (orange) hits an empty room and vanishes; the consumer sleeps forever. This is the "Spot the error" ready=true bug drawn out.
Figure 3 — the predicate loop unfolding. A single waiter's life as a flowchart-timeline: each wakeup (spurious or real) loops back to re-check the predicate. Follow the two dashed return-arrows — one for a spurious wakeup, one for a stolen item — both dump you back to "check pred", proving why while (not if) is mandatory.
Figure 4 — notify_one vs notify_all. Left panel: three sleeping consumers, notify_one wakes exactly one, the other two stay asleep. Right panel: notify_all wakes all three — the thundering herd — but only one grabs the single item; the other two re-check, find the queue empty (stolen wakeup), and sleep again.
True or false — justify
Every claim below is stated as if it were obviously true. Decide, then justify with reasoning — never a bare yes/no.
cv.notify_one() guarantees that at least one thread will eventually wake and proceed.
A condition_variable remembers how many notifies it received so a late waiter can catch up.
You must hold the mutex while calling notify_one().
Using cv.wait(lk, pred) and using while(!pred()) cv.wait(lk); are equivalent.
A spurious wakeup means the OS lost data and the program is now corrupt.
wait returns without a matching notify. The loop simply re-checks the predicate and sleeps again — no corruption, just an extra check.If only one producer and one consumer exist, if is safe instead of while.
notify_all() is always safer than notify_one(), so just use it everywhere.
notify_one() when a single waiter can handle the change; use notify_all() when the state (like done) concerns everyone.cv.wait(lk) keeps the mutex locked the whole time it sleeps.
wait atomically releases the lock and sleeps, then re-acquires it before returning (Figure 1). If it held the lock while sleeping, no notifier could ever change the state — instant deadlock.Passing a std::lock_guard to cv.wait is fine as long as you never manually unlock.
wait itself needs to unlock and relock the mutex, and lock_guard exposes no unlock. It won't even compile; you need unique_lock.Spot the error
Each snippet has a subtle bug. Name it and say what breaks.
ready = true; cv.notify_one(); (flag set with no lock held).
ready==false and calling wait, the notifier can flip ready and notify into the void; the waiter then sleeps forever. Set the flag under the same mutex.std::unique_lock<std::mutex> lk(m); if (q.empty()) cv.wait(lk); process(q.front());
if instead of a loop (Figure 3). A spurious wakeup (or a stolen item under notify_all) leaves the queue empty, and q.front() reads from an empty queue — undefined behaviour. Use cv.wait(lk, []{ return !q.empty(); });.Producer: q.push(x); then cv.notify_one(); — but q is not guarded by any mutex.
push/pop on std::queue without a lock is undefined behaviour. The mutex must protect every access, not just the wait.Waiter locks mutex A; notifier changes the flag under a different mutex B, then notifies.
cv.wait(lk, [&]{ return counter > 0; }); counter--; but counter is a plain int written by another thread without the lock.
counter under the lock but writing it lock-free is a data race; the predicate may observe an inconsistent value. Either guard all accesses with the mutex or use atomic consistently — do not mix.Consumer: lk.unlock(); cv.wait(lk); (manually unlocks the unique_lock before calling wait).
cv.wait requires the lock to be held on entry so it can perform the atomic unlock-and-sleep (Figure 1). Passing an already-unlocked lock violates the precondition.Why questions
::: answers here are the reasoning itself, not a fact to memorize.
Why does wait take a mutex reference instead of just a raw boolean flag?
Why does the standard permit spurious wakeups instead of forbidding them?
wait cheap, and the mandatory predicate loop (Figure 3) makes them harmless anyway.Why prefer unlocking before calling notify_one()?
Why can the flag, not the notification, be trusted as the source of truth?
Why does notify_all() fit the "producer is done" signal better than notify_one()?
done is a fact every consumer must observe to exit; waking only one leaves the rest asleep forever (Figure 4). State that concerns all waiters demands notify_all.Why is a busy-wait while(q.empty()){} loop worse than cv.wait, beyond just CPU?
wait sleeps at 0% CPU and yields to the producer.Why does cv.wait(lk, pred) re-acquire the lock before returning, even after a spurious wakeup?
wait runs with the shared state protected; the caller always resumes in the same "lock held, predicate true" state (Figure 1), keeping the reasoning uniform.Edge cases
The scenarios the happy path never shows you.
A notify_one() fires and no thread is waiting yet — what happens to a thread that starts waiting one microsecond later?
wait(lk, pred) sees true immediately and does not sleep; if not, it sleeps forever.Two consumers wait, producer pushes one item and calls notify_all().
The predicate is already true before the thread ever waits (item pushed before the consumer arrives).
cv.wait(lk, pred) checks the predicate first; finding it true, it returns immediately without sleeping and without needing any notify — the flag alone carried the state.The producer finishes and sets done, but a consumer is mid-processing, not waiting.
wait(lk, pred), the predicate !q.empty() || done reads done == true from shared memory and it exits cleanly.A notify_one() is issued before any producer ever changes the state, purely by mistake.
Every waiting thread wakes at once under notify_all() and they all fail their predicate.
notify_one when only one waiter can act on the change.A one-shot "go" flag is set to true, consumers notified, then a brand-new thread joins and waits.
true in shared memory, the newcomer's predicate check passes instantly. Unlike the notification, the flag is durable — this is why a level-triggered flag beats an edge-triggered notify.