Exercises — std - condition_variable
Parent: std - condition_variable · Prereqs you may want open: std::mutex, std::unique_lock, std::lock_guard, Producer-Consumer Pattern, Spurious Wakeup, Race Condition, Deadlock.
Level 1 — Recognition (can you name the pieces?)
L1.1 — Which header?
Which single header must you #include to use std::condition_variable?
Recall Solution
==<condition_variable>==. It declares both std::condition_variable and std::condition_variable_any. Note you still separately need <mutex> for the mutex it partners with.
L1.2 — Which lock type does wait demand?
cv.wait(...) takes a lock by reference. Is it a std::lock_guard<std::mutex> or a std::unique_lock<std::mutex>? Why can't it be the other one?
Recall Solution
It must be a ==std::unique_lock<std::mutex>==. Reason: wait has to unlock the mutex before sleeping (so a notifier can take the mutex and change the shared data), then re-lock it after waking. A std::lock_guard locks once in its constructor and unlocks once in its destructor — it exposes no unlock()/lock() you can call mid-life. Only std::unique_lock can be unlocked and re-locked repeatedly, which is exactly what wait needs.
L1.3 — notify_one vs notify_all
State in one line each what notify_one() and notify_all() do.
Recall Solution
notify_one() wakes exactly one waiting thread (the OS picks which). notify_all() wakes all currently-waiting threads; they then contend for the mutex one at a time.
Level 2 — Application (write the small code correctly)
L2.1 — Fix the broken wait
This code is meant to wait until ready == true. It has two bugs. Find and fix both.
std::mutex m; std::condition_variable cv; bool ready = false;
void worker() {
std::lock_guard<std::mutex> lk(m); // (A)
if (!ready) cv.wait(lk); // (B)
// ... proceed
}Recall Solution
Bug 1 (line A): lock_guard cannot be unlocked/re-locked by wait. Change to std::unique_lock<std::mutex> lk(m);.
Bug 2 (line B): if + plain wait(lk) does not survive a Spurious Wakeup or a stolen wakeup — the thread may wake with ready still false and proceed anyway. Use the predicate loop.
std::mutex m; std::condition_variable cv; bool ready = false;
void worker() {
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{ return ready; }); // == while(!ready) cv.wait(lk);
// ... proceed, mutex held
}L2.2 — The predicate, expanded
Rewrite cv.wait(lk, []{ return !q.empty(); }); using only the single-argument cv.wait(lk), no predicate overload.
Recall Solution
The predicate overload is defined as while(!pred()) cv.wait(lk);. So:
while (q.empty()) cv.wait(lk);Both are equivalent; the loop is the guarantee, not the notification.
L2.3 — One-shot "go" signal
Write a main_thread() that flips ready = true and wakes the worker from L2.1 safely (no lost wakeup).
Recall Solution
void main_thread() {
{ std::lock_guard<std::mutex> lk(m); ready = true; } // set UNDER the lock
cv.notify_one(); // then ring the bell
}Setting ready under the same mutex the waiter uses serializes "waiter checks predicate" against "notifier sets flag", closing the lost-wakeup window.
Level 3 — Analysis (reason about behaviour & timelines)
L3.1 — Why unlock before notify?
In the producer, we wrote:
{ std::lock_guard<std::mutex> lk(m); q.push(i); } // unlock here
cv.notify_one(); // notify afterExplain, referencing the timeline figure, why notifying after unlocking is an optimization, and confirm it is still correct.

Recall Solution
Correctness: the flag (q non-empty) is set under the lock, so no wakeup can be lost — the waiter, when it re-checks the predicate under the mutex, will see the item. Notify order does not affect correctness because the flag carries the truth.
Optimization (see figure): if we notify while still holding the mutex (left timeline), the woken consumer immediately tries to lock m, finds it held by the producer, and blocks again — a wasted wake→block→wake ping-pong. Notifying after unlocking (right timeline) means the consumer wakes to a free mutex and proceeds straight through. This is sometimes called the "hurry up and wait" problem, and unlocking first avoids it.
L3.2 — notify_one where notify_all is needed
Two consumers sleep on cv.wait(lk, []{ return !q.empty() || done; }). The producer finishes, sets done = true, then calls cv.notify_one(). What goes wrong?
Recall Solution
Only one consumer wakes, sees done, and exits. The other consumer is still asleep — nobody will ever notify it again (production is over), so it hangs forever (a form of Deadlock / permanent block). Fix: use cv.notify_all() on the terminal done transition so every waiter wakes, re-checks, sees done, and exits.
L3.3 — Trace the drain
Given the producer-consumer code from the parent note, the producer pushes 0,1,2,3,4. A single consumer processes them. In the consumer's inner while (!q.empty()) loop, if all 5 items happen to be present when the consumer wakes, how many values does it print in that single wake, and what is their sum?
Recall Solution
The inner while (!q.empty()) drains everything currently in the queue in one wake. If all 5 are present it prints 0,1,2,3,4 — that's 5 values, sum .
Level 4 — Synthesis (assemble a bounded system)
L4.1 — Bounded (fixed-capacity) queue
A plain queue grows without limit. Design a bounded buffer of capacity N: producers must also wait when the buffer is full. What is the minimum number of condition_variables you need, and what predicate does each waiter use?
Recall Solution
You need two condition variables sharing one mutex:
cv_not_full— producers wait on it; predicateq.size() < N.cv_not_empty— consumers wait on it; predicate!q.empty().
std::mutex m;
std::condition_variable cv_not_full, cv_not_empty;
std::queue<int> q;
const size_t N = 8;
void produce(int x) {
std::unique_lock<std::mutex> lk(m);
cv_not_full.wait(lk, []{ return q.size() < N; });
q.push(x);
lk.unlock();
cv_not_empty.notify_one();
}
int consume() {
std::unique_lock<std::mutex> lk(m);
cv_not_empty.wait(lk, []{ return !q.empty(); });
int v = q.front(); q.pop();
lk.unlock();
cv_not_empty.notify_one(); // wrong bell? see note
return v;
}Correction inside the solution: after a consumer removes an item it must ring cv_not_full.notify_one() (a slot opened), and after a producer adds an item it rings cv_not_empty.notify_one(). Each side wakes the other side. Using one CV for both would force notify_all() every time and waste wakeups — two CVs let each notify be precise. See the state diagram:

L4.2 — Capacity edge cases
For the bounded buffer above, what happens at the two extremes: (a) N = 0, and (b) N larger than the total number of items ever produced?
Recall Solution
(a) N = 0: the producer predicate q.size() < 0 is never true (size is unsigned, always ), so every producer blocks forever — a deadlock. A capacity-0 buffer is degenerate; a real "rendezvous" (size-0 handoff) needs a different design where producer and consumer meet directly.
(b) N huge: the cv_not_full predicate is always true, so producers never actually wait — the bounded buffer degrades gracefully into the unbounded producer-consumer queue from the parent note. Correct, just no back-pressure.
Level 5 — Mastery (subtle correctness & performance)
L5.1 — Predicate that reads two variables
A consumer waits with cv.wait(lk, []{ return !q.empty() || done; }). Both q and done are read inside the predicate. Is this safe? What guarantees the predicate sees a consistent, up-to-date view of both?
Recall Solution
Safe — because the predicate is always evaluated with the mutex held. wait(lk, pred) only calls pred() while lk owns m. Every writer (q.push, done = true) also holds m. So all reads and writes of q and done are serialized by the same mutex: the predicate sees a consistent snapshot. Without that shared mutex you'd have a data race and would need std::atomic plus careful memory ordering instead.
L5.2 — Timed wait return value
cv.wait_for(lk, 100ms, pred) returns a bool. After it returns, how do you distinguish "predicate became true" from "timed out"? What is the returned bool?
Recall Solution
wait_for(lk, dur, pred) returns the value of pred() as evaluated after the wait ends. So:
- returns
true⇒ the predicate is satisfied (proceed). - returns
false⇒ the timeout elapsed and the predicate is still false (handle timeout).
You do not need a separate cv_status check when using the predicate overload — the returned bool already tells you which case you're in.
L5.3 — Cost of a busy-wait vs a CV wait
Suppose a core runs cycles/second. A busy-wait while(q.empty()){} spins for 5 seconds before the item arrives; each spin iteration is 4 cycles. (a) Roughly how many wasted spin iterations is that? (b) A CV wait instead sleeps and uses ~0 CPU. State the qualitative win.
Recall Solution
(a) cycles available in 5 s cycles. At 4 cycles/iteration: That's ~2.5 billion pointless loop turns burning a full core. (b) The condition_variable wait puts the thread to sleep (OS de-schedules it), so those 5 seconds cost ~0 CPU and free the core for real work — including, crucially, the producer that must run to create the item. This is the whole reason condition_variable exists.
Recall Feynman check: say the whole ladder in one breath
A condition_variable is a doorbell with no memory. You always pair it with a shared flag and a mutex; the waiter unlocks-and-sleeps via unique_lock, re-checks the flag in a while loop (spurious/stolen wakeups), and the notifier changes the flag under the lock then rings. Two-sided flow (bounded buffer) wants two bells; edge cases (N=0, huge N) are deadlock and unbounded; and every predicate variable lives under the mutex or is atomic.