Condition Variables
A condition variable (<condition_variable>) lets a thread sleep until another thread signals that some predicate became true, without busy-waiting.
Why it matters
It is the standard tool for producer/consumer queues, thread pools, and “wait until ready” handoffs. The alternative — spinning in a while(!ready){} loop — burns a whole core and adds latency. A condition variable parks the thread in the kernel at ~zero cost until a notify wakes it, which is why every bounded work queue is built on one.
How it works
A std::condition_variable always pairs with a std::mutex and a predicate (the real shared condition). wait() atomically unlocks the mutex and sleeps, then re-locks before returning — closing the race where the signal could slip between the check and the sleep.
| Call | Effect |
|---|---|
cv.wait(lock, pred) | sleep while !pred(); re-checks on each wake |
cv.wait_for(lock, dur, pred) | as above, bounded by a timeout |
cv.notify_one() | wake one waiter |
cv.notify_all() | wake all waiters |
- The waiter needs a
std::unique_lock<std::mutex>(notlock_guard), because the CV must unlock and relock it. - Always use the predicate overload (
wait(lk, pred)); it loops internally, defeating spurious wakeups — wakes that occur with no notify, which the standard explicitly permits. - The thread that changes the state should mutate under the lock, then notify. Notifying without holding the lock is legal but risks a lost wakeup if the predicate is set non-atomically.
notify_onewakes a single waiter (use for one-item-one-consumer);notify_allis needed when the new state can satisfy several waiters or different predicates.
Example
A thread-safe queue handoff:
std::mutex m; std::condition_variable cv; std::queue<int> q;
void producer(int v) {
{ std::lock_guard lg(m); q.push(v); } // mutate under lock
cv.notify_one(); // then wake a consumer
}
int consumer() {
std::unique_lock lk(m);
cv.wait(lk, [&]{ return !q.empty(); }); // sleeps; re-locks on return
int v = q.front(); q.pop();
return v; // lock released at scope end
}If consumer used a bare cv.wait(lk) with no predicate, a spurious wake or a notify that arrived before the wait would pop an empty queue — UB.
Pitfalls
- Predicate-less
waitbreaks on spurious wakeups and lost notifications. Never omit the predicate. - Lost wakeup: if you set the flag without the mutex and the waiter checks the predicate just before
waitsleeps, thenotifyis missed and the thread sleeps forever. Mutate under the lock. notify_onewith heterogeneous waiters can wake a thread whose predicate is still false, leaving the right one asleep — usenotify_allwhen predicates differ.- Wrong lock type:
waitrequiresunique_lock; passing alock_guardwill not compile.