🔀Go deeper — read the bookA thread pool, from scratch— runnable code & full walkthrough →

multithreading · advanced

Concurrency: condition_variable — Wait Loop Pattern, Spurious Wakeup, notify_one vs all

condition_variable: thread sleeps until signaled. Requires unique_lock<mutex>. cv.wait(lk, pred): atomically releases mutex and sleeps; on wakeup reacquires mutex and rechecks pred. Equivalent to while(!pred) cv.wait(lk). Predicate handles: 1) spurious wakeups (POSIX allows wake without notify); 2) lost notifications (pred already true → returns immediately). Notifier: set state under mutex, then notify_one/all (preferred outside mutex to avoid re-sleep). notify_one: wake one waiter; notify_all: broadcast (shutdown, config). wait_for(lk, dur, pred): returns bool; true=pred satisfied, false=timeout. Use steady_clock. 3 deadly bugs: raw wait (no pred → spurious wakeup), unprotected shared flag (data race), notify inside mutex (legal but causes waiter contention). C++20 std::semaphore is simpler for counting/signaling without predicate complexity.

🔑 Key line

cv.wait(lk, pred): atomically release mutex and sleep; on wakeup: reacquire mutex + check pred; equivalent to while(!pred) cv.wait(lk); handles spurious wakeups. Set shared state under mutex BEFORE notify. notify_one/notify_all. Always use predicate form.

The code

// Condition variable — thread waits for a condition, woken by another thread
mutex mtx;
condition_variable cv;
bool ready = false;
// CORRECT: always use cv.wait with a predicate (while-loop equivalent)
void waiter() {
unique_lock<mutex> lk(mtx);
cv.wait(lk, [] { return ready; }); // spurious-wakeup safe
// ready is guaranteed true here, mutex re-acquired
}
// WRONG: raw cv.wait() without predicate — spurious wakeup bug
void waiter_buggy() {
unique_lock<mutex> lk(mtx);
cv.wait(lk); // may return even if !ready (spurious wakeup!)
// ready might be false — SILENT BUG
}
void notifier() {
{
lock_guard<mutex> lk(mtx);
ready = true;
} // set state UNDER MUTEX
cv.notify_one(); // or notify_all()
}
// cv.wait(lk, pred) is EXACTLY equivalent to:
// while (!pred()) cv.wait(lk);
// wait_for — timeout (avoid starvation):
auto status = cv.wait_for(lk, 100ms, [] { return ready; });
if (status == cv_status::timeout) { /* handle timeout */
}
// wait_until — absolute time:
cv.wait_until(lk, system_clock::now() + 1s, [] { return ready; });
// notify_one: wake ONE waiting thread
// notify_all: wake ALL waiting threads (use when any could proceed)
// DEADLOCK patterns:
// 1. notify BEFORE wait: notifier sets ready before waiter calls wait → ok ONLY with predicate
// 2. forgot to hold mutex when calling notify: data race on ready
// 3. no spurious-wakeup protection (raw wait): incorrect state after wakeup

What this lesson walks through

  1. 01condition_variable — wake thread when condition becomes true
  2. 02Spurious wakeup — the silent bug in raw cv.wait()
  3. 03The lost notification — when notifier runs before waiter
  4. 04notify_one vs notify_all — which to use
  5. 05wait_for and wait_until — timeouts
  6. 06Common cv pitfalls — the 3 deadly bugs

condition_variable allows a thread to sleep until another thread signals it. It requires a mutex (unique_lock). The canonical pattern: waiter acquires mutex, calls cv.wait(lk, predicate) — this atomically releases the mutex and sleeps; when woken, reacquires mutex and rechecks predicate. The notifier sets shared state under the mutex, then calls notify_one(). Without the predicate, spurious wakeups cause silent bugs.

See it animated — step by step, at your own pace

Unlock the full interactive walkthrough of Concurrency: condition_variable — Wait Loop Pattern, Spurious Wakeup, notify_one vs all and 100+ animated C++ interview lessons.

← Previous
Concurrency: Thread Pool — Implementation, packaged_task, Shutdown, Pitfalls
Next →
Atomics & Memory Ordering (acquire/release)