Condition Variables and the Wait-Signal Pattern
What This Concept Is
A condition variable is a coordination primitive used together with a mutex to let threads wait for a predicate on shared state to become true.
Its API (pthread form):
cond_wait(cv, mutex)-- atomically releasemutex, put the caller to sleep oncv, reacquiremutexwhen woken.cond_signal(cv)-- wake one thread waiting oncv(if any).cond_broadcast(cv)-- wake all waiters.
The atomicity of "release the mutex and sleep" is the whole point. If wait were implemented as unlock; sleep;, the signal could arrive between the two steps and be lost.
The correct usage pattern is:
pthread_mutex_lock(&m);
while (!predicate()) {
pthread_cond_wait(&cv, &m);
}
// predicate is now true AND we hold m
...
pthread_mutex_unlock(&m);
The while (not if) is non-negotiable.
Why It Matters Here
Condition variables are how threads wait for logical conditions rather than for a lock to be free. Every producer-consumer buffer, every thread pool idle-wait, every job queue, every "finished" signal is built on them.
They are also the most commonly-misused concurrency primitive in real code. Almost all of the misuse comes from three mistakes:
- Calling
waitwithout holding the mutex. - Using
ifinstead ofwhile. - Signaling without updating shared state under the mutex.
Concrete Example
Producer-consumer bounded buffer with one mutex and two condition variables:
int buf[N];
int count = 0, head = 0, tail = 0;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t not_full = PTHREAD_COND_INITIALIZER;
pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER;
void put(int x) {
pthread_mutex_lock(&m);
while (count == N) pthread_cond_wait(¬_full, &m);
buf[tail] = x;
tail = (tail + 1) % N;
count++;
pthread_cond_signal(¬_empty);
pthread_mutex_unlock(&m);
}
int get(void) {
pthread_mutex_lock(&m);
while (count == 0) pthread_cond_wait(¬_empty, &m);
int x = buf[head];
head = (head + 1) % N;
count--;
pthread_cond_signal(¬_full);
pthread_mutex_unlock(&m);
return x;
}
This is the reference bounded buffer. Nearly every correct variant is structurally the same.
Common Confusion / Misconception
"if should work because a signal means a thread was woken." No. Pthreads condition variables have Mesa semantics: when a waiter wakes, it re-contends for the mutex and by the time it acquires it, another woken thread may have consumed the resource. The waiter must recheck the predicate. Spurious wakeups (waking with no signal at all) are also allowed by the spec. A while handles both.
"I should always broadcast; it is safer." Broadcast wakes every waiter, which then all re-contend for the mutex. With a large waiter set, this is a thundering-herd disaster. Use signal when only one waiter can make progress; use broadcast when the condition change affects all waiters (for example, shutdown, or a buffer state change where multiple waiters with different predicates share the same cv, which is itself a design smell).
"I can signal without holding the mutex." You can, but then the signaler might race with the waiter's predicate check. Always update shared state, then signal, all under the mutex.
How To Use It
Every correct wait-signal pattern has three paired pieces:
- A predicate on shared state (for example,
count > 0). - A mutex protecting every access to that state.
- A condition variable paired with the mutex, signaled whenever the predicate could become true.
When the code reviewer asks "can this miss a wakeup?", your answer must point at the atomic release-mutex-and-sleep and at the while loop.
Check Yourself
- Why must
cond_waitatomically release the mutex and sleep? - Why must the predicate be checked in a
whilerather than anif? - When should you
broadcastinstead ofsignal?
Mini Drill or Application
Rewrite the bounded buffer above to use a single condition variable instead of two. Identify the schedule where a producer's signal wakes another producer that still cannot proceed, and explain why while saves you.