Skip to article
ALGORITHMICSConcurrency
Concurrency5 min read

Condition Variables

Waiting for a state, not a lock — and why the check has to be a while loop.


A mutex answers “may I touch this?”. It cannot answer “is there anything to touch yet?”.

A consumer with an empty queue does not want the lock — it wants the queue to stop being empty. Spinning on it is the obvious approach and the wrong one:

while (queue.isEmpty()) { /* burn a core */ }

That is 100% CPU to learn nothing.

The primitive

A condition variable lets a thread sleep until another thread says the state changed.

// Consumer
lock();
while (queue.isEmpty()) {
condition.wait(); // releases the lock and sleeps, atomically
}
const job = queue.shift();
unlock();
// Producer
lock();
queue.push(job);
condition.notify(); // wake one waiter
unlock();
1 / 7
queue empty
consumer checking
lock();
while (queue.isEmpty()) cond.wait();
job = queue.shift();
unlock();

Consumer takes the lock and checks the queue. Empty.

while, never if

This is the rule people break, and it is worth understanding rather than memorising.

The general shape is: the condition variable is a hint that something may have changed. The predicate is the truth.

notify or notifyAll

notify() wakes one waiter. notifyAll() wakes all of them.

Use notify() when every waiter is waiting for the same thing and any one can proceed — a pool of identical consumers.

Use notifyAll() when waiters are waiting for different predicates. With one condition variable shared between “queue not empty” and “queue not full”, notify() can wake a thread whose predicate is still false while the one that could proceed stays asleep. That is a lost wakeup, and the program stops.

The safer default is notifyAll(); the faster one is a separate condition variable per predicate, which is what a bounded queue actually does.

Prefer something higher level

Almost every use of a condition variable is one of these, and the library version is already correct:

Reach for a raw condition variable when your predicate is genuinely custom — “wait until the buffer has at least 4 KB and the connection is still open” — and there is no ready-made structure for it.