When is a condition variable needed, isn't a mutex enough?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
A mutex alone protects shared data from concurrent access, but it cannot make a thread wait efficiently until a certain condition becomes true. A condition variable lets a thread sleep until another thread signals that the condition has changed — without busy-waiting (spinning in a loop). Without condition variables, a thread would have to repeatedly lock the mutex, check the condition, unlock, and loop, wasting CPU cycles. Condition variables are essential for producer-consumer queues, thread pools, barriers, and any pattern where one thread must wait for another thread's action.
Mutex Only: Busy-Waiting Problem
A mutex protects shared state but cannot signal state changes:
The consumer constantly acquires and releases the lock even when there is nothing to process.
Condition Variable: Efficient Waiting
A condition variable lets the consumer sleep until the producer signals new data:
Key difference: cv.wait() releases the mutex and puts the thread to sleep with zero CPU usage. When notify_one() is called, the thread wakes up, re-acquires the mutex, and checks the condition.
Why the Predicate Matters
The second argument to cv.wait(lock, predicate) protects against spurious wakeups — the OS may wake a thread even without a notification:
Always use a predicate with wait().
Producer-Consumer with Bounded Buffer
A complete example with a fixed-size buffer using two condition variables:
Two condition variables are needed because producers and consumers wait for different conditions.
Python Example
Java Example
In Java, every object has an intrinsic condition variable accessed through wait() and notifyAll() inside synchronized blocks.
When You Need Each
| Scenario | Mutex Alone | Condition Variable + Mutex |
| Protect shared counter | Sufficient | Not needed |
| Producer-consumer queue | Busy-wait required | Efficient wait |
| Thread pool task dispatch | Busy-wait required | Efficient wait |
| Barrier synchronization | Cannot implement | Required |
| One-time initialization | std::once_flag | Not needed |
Common Pitfalls
- Forgetting the predicate in
cv.wait(): Without a predicate, the thread may proceed on a spurious wakeup when the condition is not actually met. This causes consuming from an empty queue or writing to a full buffer. Always pass a lambda predicate:cv.wait(lock, [&]{ return condition; }). - Calling
notifybeforewait: If the producer callsnotify_one()before the consumer callswait(), the signal is lost. The consumer then waits forever (deadlock). Design your logic so the condition check in the predicate handles this — if the condition is already true,waitreturns immediately. - Using
notify_onewhen multiple threads are waiting:notify_onewakes only one thread. If multiple consumers are waiting and one notification should wake all of them (e.g., shutdown signal), usenotify_all(). - Holding the mutex while doing expensive work: Lock the mutex only to check the condition and access shared data. Release it before doing CPU-intensive or I/O work. Holding the lock during processing blocks all other threads from making progress.
- Using a condition variable without a mutex: Condition variables must always be used with a mutex that protects the shared state being checked. Signaling or waiting without the mutex causes data races and undefined behavior.
Summary
- A mutex protects shared data; a condition variable enables efficient waiting for state changes
- Without condition variables, threads must busy-wait (spin), wasting CPU cycles
- Always use a predicate with
cv.wait()to handle spurious wakeups - Use
notify_one()to wake a single waiter,notify_all()to wake all waiters - The producer-consumer pattern is the canonical use case — one thread produces data and signals, the other waits and consumes

