AlgoMaster Logo

Condition Variables

Medium Priority18 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

Sometimes a thread cannot make progress until something happens elsewhere: an order is marked ready, inventory is restocked, a payment confirmation arrives. The naive fix is a loop that keeps checking a flag, which burns CPU and races with the writer. std::condition_variable is the standard tool that lets a thread sleep until another thread wakes it, atomically tied to a mutex so no notification is lost. This chapter covers wait, wait_for, wait_until, notify_one, notify_all, spurious wakeups, and why the predicate form of wait is not optional.

The Problem with Busy-Waiting

Consider a worker thread that has to wait until a customer's order is marked "ready" by another thread. The first instinct is usually a flag and a loop.

This compiles and might appear to run correctly. It has two serious problems.

The first is performance. The courier thread spins through the while loop millions of times per second, pinning a CPU core at 100% while waiting. On a laptop that means a hot fan; on a server farm it means a real bill. The thread is not doing useful work; it is asking the same question over and over.

The second is correctness. orderReady is read by one thread and written by another, with no synchronisation between them. That is a data race, which is undefined behaviour. The compiler is free to hoist the read out of the loop, deciding that orderReady cannot change inside the body and looping forever. Even if that does not happen, the courier might see a stale cached value of orderReady for an unbounded time on multi-core hardware. The program "works" by luck on small examples and fails in production.

A busy-wait loop typically runs at hundreds of millions of iterations per second per core. Even when it produces the right answer, it is burning energy and crowding out threads that have real work to do. Sleep-based waiting (which condition variables provide) costs essentially nothing while the thread is parked.

You cannot fix this with volatile either. volatile tells the compiler not to optimise the variable away, but it does not provide cross-thread synchronisation or memory ordering. The right answer is std::condition_variable.

What std::condition_variable Solves

A condition variable is a coordination primitive defined in <condition_variable>. It lets one or more threads sleep on a mutex-protected condition and lets another thread wake them up when the condition might have changed. The key word there is "sleep". A waiting thread releases the CPU entirely until the kernel hands it back, so it costs essentially nothing while it waits.

The pattern always involves three things working together:

  1. A shared piece of state (a flag, a counter, a queue).
  2. A std::mutex that protects that state from data races.
  3. A std::condition_variable that threads use to wait for the state to change.

The busy-wait example rewritten properly:

The courier thread sleeps inside cv.wait until the kitchen calls cv.notify_one(). While it sleeps, it uses zero CPU. The mutex protects every access to orderReady, so there is no data race. The compiler cannot hoist anything past the lock or the wait, because both have well-defined synchronisation semantics. This is correct, efficient, and roughly the same number of lines.

The next sections unpack each piece: why wait needs a unique_lock, what the predicate is for, what "spurious wakeup" means, and when to use notify_one versus notify_all.

How wait Works with unique_lock

The strangest line in the previous example is cv.wait(lock, ...). Why does wait need to be handed a lock? The answer is that condition variables and mutexes are deliberately fused together to close a race that would otherwise be unfixable.

What wait actually has to do:

  1. Check the condition (orderReady == true).
  2. If the condition is false, go to sleep until someone notifies.
  3. When woken, reacquire the mutex and check the condition again.

Between steps 1 and 2 there is a danger zone. If the courier checks orderReady, sees false, and then the kitchen runs to completion and calls notify_one before the courier goes to sleep, the notification is lost. The courier sleeps forever waiting for a notification that already happened.

condition_variable::wait solves this by doing two things atomically: it releases the mutex and puts the thread to sleep in one indivisible step. The kernel sees the courier holding the lock when it starts waiting, so the kitchen cannot enter the critical section until the courier is fully parked. By the time the kitchen acquires the lock to set orderReady = true and call notify_one, the courier is guaranteed to be on the waiter list.

When the kitchen calls notify_one, the courier wakes, reacquires the mutex (waiting if necessary), re-checks the predicate, and either proceeds or goes back to sleep.

The atomic unlock-and-sleep cannot be built out of mutexes and flags alone. It has to be a primitive the standard library provides, which is why condition variables exist as their own type.

std::lock_guard will not work here because wait needs to unlock and relock the mutex during its operation, and lock_guard does not expose those operations. std::unique_lock does, which is why wait requires std::unique_lock<std::mutex> specifically.

A sleeping thread costs near-zero CPU but does occupy kernel resources (a thread control block and a small stack). Waking a thread is on the order of microseconds because it involves a context switch. For very fine-grained, very short waits, spinning briefly before sleeping can be faster, but for anything longer than a few microseconds, wait is the right answer.

Spurious Wakeups and the Predicate Form

A thread sleeping in cv.wait can wake up for no reason at all, even when nobody called notify_one or notify_all. The standard explicitly permits this. It is called a spurious wakeup, and it is not a bug in the implementation. It is a deliberate design choice that lets wait be implemented efficiently on systems where the underlying primitive (like POSIX pthread_cond_wait) is also allowed to wake spuriously.

This means you cannot trust the fact that wait returned as proof that the condition is true. You have to check.

The wrong way:

If the thread wakes spuriously, it prints "Order is ready!" while orderReady is still false. The predicate problem is not just spurious wakeups. Even when a notification is real, the condition could have changed back between the wake and the time this thread reacquires the lock. Multiple consumers waking up can race for the same single notification.

The right way is the predicate form of wait:

This is equivalent to:

The lambda is evaluated each time the thread wakes. If the predicate returns false, wait goes back to sleep. The thread only exits the call when the predicate is true. The "while not predicate, wait" loop defends against both spurious wakeups and stale conditions.

The predicate form is so important that the unconditional form is almost always a bug in real code. If you find yourself writing cv.wait(lock) without a predicate, stop and think about whether you have handled spurious wakeups some other way. Usually you have not.

notify_one vs notify_all

When a thread changes the shared state, it tells the condition variable to wake waiters. There are two ways to do that.

cv.notify_one() wakes one waiting thread (which one is unspecified, often the one that has been waiting longest, but you cannot rely on that). cv.notify_all() wakes every thread currently waiting on this condition variable.

The choice depends on whether more than one waiter could usefully proceed.

If only one thread can act on the state change, use notify_one. Example: a single order is now ready, and any one of several available couriers can pick it up. Waking all of them just to have all-but-one go straight back to sleep wastes context switches.

If the state change might allow multiple waiters to proceed, use notify_all. Example: inventory has been restocked from 0 to 100 units, and there are many backordered orders waiting. Waking only one would leave the rest sleeping even though there is stock for them.

Three buyers each want a different quantity, and there is now enough stock for all of them. notify_all wakes all three so they can each grab what they need. With notify_one, only one buyer would wake; the others would keep sleeping until somebody else managed to notify again.

Thundering herd cost. notify_all can wake every waiter at once, but only one can hold the mutex at a time. The others reacquire the lock one by one, re-check the predicate, and (depending on how the state evolved) often go straight back to sleep. With many waiters, this generates a burst of context switches and lock contention. If only one waiter could possibly proceed, notify_one avoids the herd entirely. Use notify_all only when multiple waiters could genuinely make progress.

The rule of thumb: notify_one for "one resource is now available", notify_all for "the world changed and several waiters might care".

A One-Shot Start Signal

A common pattern is one main thread coordinating a fleet of workers and signalling "start" to all of them at once. Each worker spins up, does setup, then waits for the green light. The main thread flips a flag and calls notify_all.

Two details worth pointing out. First, the mutex is held when startProcessing = true is assigned. That is required: the flag is shared state, and the assignment needs to be ordered with the agents' reads of it. Second, notify_all is called after the mutex is released (the lock_guard goes out of scope at the end of the inner block). Notifying with the mutex held is correct but slightly inefficient: the woken thread tries to reacquire the lock and immediately blocks because the notifier still owns it. Notifying outside the critical section lets the woken thread grab the lock immediately.

What if the manager runs to completion (sets the flag and notifies) before any agent has reached its wait call? With the predicate form, that is fine. When the agent finally calls wait, it evaluates the predicate, sees startProcessing == true, and returns immediately without sleeping. The notification was "stored" in the predicate state, not in the condition variable itself. This is another reason the predicate form is mandatory: it makes the code correct regardless of the ordering between the notify and the wait.

Timed Waits: wait_for and wait_until

Sometimes you do not want to wait forever. A customer placing an order might give the payment system up to 30 seconds to confirm before giving up. wait_for and wait_until are the timed variants of wait.

wait_for(lock, duration) waits up to a relative duration (like "200 milliseconds from now"). wait_until(lock, time_point) waits until an absolute time point (like "until 12:00:00 UTC"). Both also have predicate forms that work the same way as wait.

The payment gateway takes 500ms to respond, but the order is only willing to wait 300ms. The order thread wakes up, sees the predicate is still false, and wait_for returns false to indicate timeout. Had the gateway responded within 300ms, the predicate would be true and wait_for would return true.

The return value of the predicate form is the predicate's final value, not a separate "timed out or not" status. That keeps the check simple: if the predicate is true, you got what you were waiting for; if it is false, you timed out.

The non-predicate form of wait_for returns a std::cv_status instead, with values cv_status::no_timeout and cv_status::timeout. That form requires you to re-check the condition manually, so the predicate form is the cleaner choice almost always.

wait_until is useful when the deadline is fixed regardless of how long preparation took. For instance, "wait until exactly 2:00 PM" rather than "wait up to 30 minutes from now". You build a std::chrono::time_point and pass it in.

Use steady_clock for deadlines that should track elapsed time even if the system clock changes. Use system_clock only when the deadline is an actual calendar time (which is rare for low-level synchronisation).

wait_for and wait_until are essentially the same as wait plus a kernel timer. They cost negligibly more while sleeping, but every wake (whether from a notify, a timeout, or a spurious wakeup) involves a system call to check the time and decide what to do next. Do not put a 1ms timeout in a tight loop and expect it to be free.

condition_variable_any

std::condition_variable only works with std::unique_lock<std::mutex>. That is an intentional restriction: it lets the implementation be lean and fast. If you need to wait while holding a different kind of lock, such as a std::shared_lock (for reader-writer mutexes) or a custom lock type, the standard provides std::condition_variable_any.

The interface is identical: wait, wait_for, wait_until, notify_one, notify_all. The only difference: the wait family is templated on the lock type, so it accepts anything that supports lock() and unlock() member functions.

condition_variable_any is more flexible but slightly heavier than condition_variable. The implementation typically uses an internal mutex to coordinate with the external lock, which adds a small amount of overhead per wait. Use the plain condition_variable when you can; use condition_variable_any only when the lock type forces your hand.

In practice, the vast majority of condition variable usage involves a plain std::mutex and a std::unique_lock, so condition_variable is the right default.

Common Mistakes

A few mistakes show up over and over with condition variables. Knowing the shapes makes them easy to spot.

Forgetting the predicate. Calling cv.wait(lock) with no predicate leaves the code vulnerable to spurious wakeups and to notifications that race with state changes. Always pass a predicate.

Modifying the shared state without holding the mutex. The mutex is not just for the wait side. The thread that sets the condition must also hold the lock while writing the state. Otherwise the waiter's predicate re-check can race with the write.

Notifying with no shared-state change. A notification with no corresponding state change wakes the waiter, who re-checks the predicate, finds it still false, and goes back to sleep. Harmless but wasteful. Always pair a notify with the state change it announces.

Using `lock_guard` with `wait`. This does not compile because wait needs to call unlock and lock on the lock object, which lock_guard does not expose. The compiler error mentions a missing member function on the lock type. Switch to unique_lock.

Notifying while holding the mutex. Not a bug, but a minor inefficiency. The woken thread wakes up, tries to grab the mutex, finds it held by the notifier, and goes straight back to sleep until the notifier releases. Release first, then notify:

The optimisation is small and many codebases do not bother, but it is useful to know about when profiling reveals contention.

Quiz

Condition Variables Quiz

8 quizzes