5.2.25C++ Programming

std - condition_variable

1,983 words9 min readdifficulty · medium

WHY does this exist?

WHAT it is: a synchronization primitive (#include <condition_variable>) that works together with a std::mutex (via std::unique_lock) to let threads wait for a condition.

HOW the three pieces fit:

  1. A shared condition (e.g. bool ready, a queue) protected by a mutex.
  2. Waiter: locks mutex → calls cv.wait(lock, pred) → atomically unlocks + sleeps.
  3. Notifier: locks mutex → changes condition → calls cv.notify_one()/notify_all().
Figure — std - condition_variable

The core API (derived by reasoning, not memorized)

Why is the predicate while, not if? Two reasons baked into the standard:

  1. Spurious wakeups: the OS may wake a waiting thread even though nobody notified. The loop re-checks and goes back to sleep.
  2. Stolen wakeups: with notify_all, several threads wake, but the first one to grab the lock may consume the only item. The loop makes the others re-check and sleep again.

Deriving the producer–consumer queue from scratch

We need: shared std::queue<int>, a std::mutex m, a std::condition_variable cv.

#include <condition_variable>
#include <mutex>
#include <queue>
#include <thread>
#include <iostream>
 
std::queue<int> q;
std::mutex m;
std::condition_variable cv;
bool done = false;
 
void producer() {
    for (int i = 0; i < 5; ++i) {
        {
            std::lock_guard<std::mutex> lk(m); // lock to mutate shared state
            q.push(i);
        } // unlock BEFORE notify (small optimization, avoids waiter waking & blocking)
        cv.notify_one();                       // ring the doorbell
    }
    {
        std::lock_guard<std::mutex> lk(m);
        done = true;
    }
    cv.notify_all();                           // wake everyone so they can exit
}
 
void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lk(m);    // unique_lock: wait can unlock/relock it
        cv.wait(lk, []{ return !q.empty() || done; }); // sleep until pred true
        while (!q.empty()) {
            int v = q.front(); q.pop();
            std::cout << "got " << v << "\n";
        }
        if (done && q.empty()) break;          // nothing left and producer finished
        // lk auto-unlocks at end of loop scope
    }
}

Second example: a one-shot "go" signal


Common mistakes (Steel-manned)


Flashcards

What header declares std::condition_variable?
<condition_variable>
Which lock type must a thread pass to cv.wait and why?
std::unique_lock<std::mutex>, because wait must atomically unlock then re-lock the mutex (lock_guard can't).
What does cv.wait(lock) do atomically?
Unlocks the mutex, puts the thread to sleep until notified, then re-acquires the mutex before returning.
Why must the wait condition be checked in a loop (or via the predicate overload)?
To handle spurious wakeups and stolen wakeups (another thread consuming the resource first).
Difference between notify_one and notify_all?
notify_one wakes a single waiting thread; notify_all wakes all of them (they then contend for the lock).
Why modify the shared state while holding the mutex before notifying?
To avoid the lost-wakeup race where the waiter misses the notification and sleeps forever.
Does a condition_variable remember notifications sent when no one is waiting?
No — notifications are lost if no thread is currently waiting; correctness must come from the shared predicate/flag.
Is it necessary to hold the lock while calling notify?
Not required, but the safe pattern is to change state under the lock; you may unlock before notifying as an optimization.
What's cv.wait(lk, pred) equivalent to?
while(!pred()) cv.wait(lk);

Recall Feynman: explain to a 12-year-old

Imagine you're a worker waiting for pizza. Instead of standing at the oven checking every second (tiring = wasted energy), you go nap on the couch. There's a rule: before napping you tell the chef "wake me when it's ready" — and you let go of the kitchen so the chef can cook. When the pizza is done the chef shouts ("notify!") and you wake up, double-check the pizza is really there (maybe you just dreamed the shout — that's a spurious wakeup), and then eat. The couch nap = wait, the shout = notify, double-checking = the while loop. That way you use no energy while sleeping but never miss your pizza.


Connections

  • std::mutex — the lock the condition_variable cooperates with.
  • std::unique_lock — required by wait; enables unlock/relock.
  • std::lock_guard — simpler RAII lock, used by the notifier side.
  • Producer-Consumer Pattern — the canonical use case.
  • std::future and std::promise — higher-level one-shot signaling.
  • Spurious Wakeup — the reason for the predicate loop.
  • std::atomic — alternative for lock-free flags (but no efficient blocking).
  • Race Condition / Deadlock — the bugs proper use prevents.

Concept Map

wastes CPU

solved by

works with

requires

guards

allows unlock plus relock

atomically

loops on predicate

wakes one

wakes all

changed then signaled by

builds

Busy wait loop

Efficient blocking needed

std::condition_variable

std::mutex

std::unique_lock

Shared condition

cv.wait lock, pred

Unlock then sleep then relock

Handles spurious and stolen wakeups

notify_one

notify_all

Producer consumer queue

Hinglish (regional understanding)

Intuition Hinglish mein samjho

Socho ek consumer thread ko queue se item chahiye, lekin queue khaali hai. Agar wo while(empty){} karke baar-baar check karta rahe, to CPU 100% jal jayega — ye busy-wait bekaar hai. std::condition_variable ka kaam yahi hai: thread ko so jaane do (0% CPU) jab tak condition true na ho. Jab producer item daal de, wo doorbell bajata hai yaani notify_one(), aur sleeping thread jaag jaata hai.

Important baat: wait() ko std::unique_lock chahiye, lock_guard nahi. Kyunki wait ko mutex unlock karke sona padta hai (taaki producer lock le sake aur data change kar sake), phir jagne par dobara lock karta hai. Aur hamesha predicate wali wait use karo: cv.wait(lk, []{ return !q.empty(); });. Ye andar se while loop hai — spurious wakeup (bina notify ke OS jaga de) aur stolen wakeup (doosra thread item le jaaye) dono se bachata hai.

Sabse badi galti: shared flag ko bina mutex ke change karna. Isse lost wakeup ho sakta hai — waiter ne check kiya flag false hai, abhi sleep karne hi waala tha, tab tak notifier flag set karke notify kar deta hai, par koi sun nahi raha — waiter hamesha ke liye so gaya. Fix: flag ko hamesha usi mutex ke andar change karo jo waiter use karta hai. Yaad rakho: condition_variable ki koi memory nahi hoti — agar notify tab bheja jab koi wait nahi kar raha, wo notify gum ho jaata hai. Isliye correctness shared flag se aati hai, notification se nahi.

Mantra: "Wait under lock, wake with notify, aur predicate ko hamesha loop mein check karo."

Go deeper — visual, from zero

Test yourself — C++ Programming

Connections