JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Condition Variables and Waiting Correctly

Guide 1 gave you the mutex to keep two threads from colliding. But a lock cannot tell a thread to wait for something to become true — a queue to fill, a flag to flip. That is what a condition variable does, and getting the wait pattern exactly right is one of the most quietly error-prone corners of concurrency.

The problem a lock cannot solve

From guide 1 you know what a mutex does: it protects a critical section so that only one thread at a time touches some shared data. That is mutual exclusion, and it solves collisions. But there is a second, different need that a lock alone cannot meet: waiting for a condition to become true. Imagine a shared queue that a worker thread pulls jobs from. When the queue is empty, the worker has nothing to do — it must wait until some other thread adds a job. A lock can guard the queue, but it has no way to say "sleep until there is work, then wake me".

The naive fix is to spin: lock, check if the queue is empty, unlock, and loop again — over and over until something appears. This works, but it is wasteful in the worst way. A busy-wait loop burns a whole CPU core doing nothing, just asking "is it ready yet? is it ready yet?" millions of times a second. On a laptop that means a hot fan and a dead battery; on a server it means a core stolen from real work. You met the same temptation with the spinlock in guide 1 — spinning is only ever justified when the wait is known to be a few instructions long. Waiting for a human to type, or for a job that may not arrive for seconds, is not that case.

What we actually want is for the waiting thread to go to sleep — to be set aside by the scheduler, consuming zero CPU — and to be woken only when the thing it is waiting for might have changed. That is precisely the job of a condition variable: it is the bridge between "I am holding a lock and the world is not yet how I need it" and "some other thread just changed the world; wake the sleepers".

What a condition variable is, and is not

A condition variable is a small object that lets a thread wait until another thread signals that something may have changed. It always works in partnership with a mutex — never alone. The core operations are three: `wait`, which atomically releases the mutex and puts the calling thread to sleep; `signal` (sometimes called `notify_one`), which wakes one waiting thread; and `broadcast` (`notify_all`), which wakes them all. In POSIX threads these are pthread_cond_wait(), pthread_cond_signal(), and pthread_cond_broadcast().

The first thing to be exact about: a condition variable carries no value and remembers nothing. It is not a flag you can read; it is not a boolean. The actual condition — "is the queue non-empty?" — lives in your own shared variable, protected by the mutex. The condition variable is purely a waiting room attached to that data. This matters because of a hard consequence: if a thread signals a condition variable when no one is currently waiting, the signal is simply lost — it is not saved up for the next thread to arrive. This is the single biggest difference from a semaphore (guide 3), which does remember counts. Mix the two up and you will write subtle bugs.

The atomic release-and-sleep that makes it work

Here is the subtle part, and it is worth slowing down for. A waiting thread holds the mutex (it had to, in order to safely look at the shared data and discover the condition is false). But if it just went to sleep while holding the lock, no other thread could ever acquire that lock to change the data and signal — everyone would be stuck forever. So `wait` must do two things as one indivisible step: release the mutex and put the thread to sleep. When it is later woken, it does the reverse: re-acquires the mutex before returning, so the thread is back to holding the lock exactly as it did before, free to look at the data again.

Why must release-and-sleep be atomic — a single uninterruptible step? Picture the alternative, done in two separate steps: the thread unlocks the mutex, and then, a moment later, calls sleep. In the gap between those two steps another thread could grab the lock, add a job, and signal — but our thread has not yet gone to sleep, so it misses the signal entirely, and then it sleeps, now waiting for a wake-up that already happened and will never come again. This is the lost-wakeup problem, and it is exactly the race the condition variable's design exists to prevent. By folding unlock and sleep into one atomic operation, pthread_cond_wait() leaves no gap for a signal to slip through.

The wait-while-predicate pattern (the one rule)

There is one pattern you must internalize, and breaking it is the most common condition-variable bug in the world. You do not call wait once and trust that when it returns, your condition is true. Instead you wait in a loop, re-checking the predicate every time you wake. In words: *while the condition I need is still false, wait.* Only when the loop's test finally fails — meaning the condition is now true — do you fall through and proceed, still holding the mutex.

/* consumer: wait until the queue has an item, then take one */
pthread_mutex_lock(&m);
while (count == 0) {            /* WHILE, not if */
    pthread_cond_wait(&cv, &m); /* atomically: unlock m, sleep,
                                  wake, relock m */
}
item = take_from_queue();       /* safe: we hold m and count > 0 */
count--;
pthread_mutex_unlock(&m);

/* producer: add an item, then wake a waiter */
pthread_mutex_lock(&m);
add_to_queue(item);
count++;
pthread_cond_signal(&cv);       /* wake one consumer */
pthread_mutex_unlock(&m);
The canonical shape. Note the consumer uses while, never if. The shared predicate (count) is read and written only while the mutex is held — that is the lock invariant from guide 1, still in force.

There are two reasons the loop must be a `while`, not an `if`. First, spurious wakeups: a spurious wakeup is when pthread_cond_wait() returns even though nobody signaled — the POSIX standard explicitly permits this, because forbidding it would make the implementation slower on some hardware. So a return from wait means "maybe something changed", never "definitely". Second, stolen wakeups: even after a real signal, another thread might wake first, grab the mutex, and consume the very item you were signaled about, leaving the queue empty again by the time you re-acquire the lock. In both cases the cure is identical — re-check the predicate, and if it is still false, simply wait again. The loop turns "maybe" into a guarantee.

Signal, broadcast, and where this is heading

When you change the shared state and want to wake waiters, you choose between `signal` and `broadcast`. Signal wakes (at least) one waiting thread; use it when any single waiter can make progress and waking one is enough — adding one job to the queue, for instance, since only one consumer can take it. Broadcast wakes all waiters; use it when the change might let several of them proceed, or when different waiters are waiting on different predicates that share one condition variable. A safe-but-slower rule of thumb when unsure: broadcast is always correct (the woken threads that still find their predicate false simply loop and wait again, thanks to the `while`), whereas signal can be too few. Prefer signal for efficiency once you are sure one wake suffices.

One honest caveat about ordering. A condition variable makes no promise about which waiter wakes or in what order — there is no fairness guarantee, so a particular thread can in principle be passed over many times. Usually this is fine; when it is not, you have stepped into starvation territory, a concern guide 5 and the deadlock discussion will return to. And do not be tempted to signal without holding the mutex to "save a lock": while POSIX technically permits it, signaling inside the lock is the simple, race-free habit, and the cost is negligible.

Step back and see the shape you have built. A mutex plus a condition variable plus a shared predicate is exactly the classical monitor — the monitor pattern that languages like Java bake into `synchronized`, and that C++ offers as std::condition_variable with std::unique_lock. You now have everything needed for the central example of this whole rung: the producer-consumer problem, where producers add to a buffer and consumers remove from it, each waiting politely when the buffer is empty or full. Guide 3 introduces the semaphore as an alternative tool that counts, and guide 4 puts producer and consumer together for real. The wait-while-predicate loop you learned here is the beating heart of all of it.