Loading learning content...
Condition variables and mutexes are inseparable partners in the dance of synchronization. Unlike semaphores, which are self-contained synchronization primitives, condition variables cannot function alone. Every condition variable operation—especially wait—requires an associated mutex.
But why this marriage? What makes condition variables so dependent on mutexes that they cannot exist independently?
The answer lies in the fundamental problem condition variables solve: waiting for a predicate on shared state. The predicate involves shared data. Shared data requires mutual exclusion. Therefore, checking and modifying the predicate must happen while holding a lock. The condition variable provides the wait-and-signal mechanism; the mutex provides the protection for the predicate.
In this page, we'll explore this relationship in depth—why it exists, how to maintain it correctly, what happens when it's violated, and advanced patterns that leverage it for elegant concurrent code.
By the end of this page, you will understand: why condition variables require mutexes; the invariants that must hold for correct operation; common violations and their consequences; proper pairing patterns; and how different languages handle the CV-mutex relationship.
To understand why condition variables require mutexes, let's trace through the logic step by step.
Step 1: You're waiting for shared state to reach a certain condition.
Example: "buffer is not empty" or "worker threads are all idle."
Step 2: To check the condition, you must read shared state.
Reading count > 0 or idle_workers == total_workers accesses shared variables.
Step 3: Reading shared state without synchronization is a data race.
If another thread modifies count while you read it, you get undefined behavior.
Step 4: Therefore, you need a lock when checking the condition.
The mutex provides this protection.
Step 5: If the condition is false, you need to wait.
But you can't hold the mutex while waiting—that would prevent anyone from modifying the state to make the condition true.
Step 6: You must release the mutex when you wait.
This allows other threads to acquire it and modify the state.
Step 7: After waking up, you must re-check the condition.
Mesa semantics and spurious wakeups mean the condition might not be true.
Step 8: Re-checking requires holding the mutex again.
The wait operation must re-acquire the mutex before returning.
Conclusion:
The mutex is integral to the entire wait pattern. The condition variable merely provides the block-and-wake mechanism; the mutex provides the protection for the predicate.
1234567891011121314151617181920212223242526272829303132
// This code demonstrates why the mutex is inseparable // The predicate involves shared stateint count; // Shared!pthread_mutex_t mutex;pthread_cond_t cond; void waiter(void) { pthread_mutex_lock(&mutex); // MUST lock to read count while (count == 0) { // Check predicate (reads count) // Inside wait: // 1. Release mutex (so others can modify count) // 2. Block until signaled // 3. Re-acquire mutex (so we can safely re-check count) pthread_cond_wait(&cond, &mutex); // Back here: we hold mutex, can safely check count } // count > 0, we hold mutex, safe to proceed count--; // Modify shared state pthread_mutex_unlock(&mutex);} // Every step requires the mutex:// - Reading count to check predicate// - Modifying count when proceeding// - Re-checking count after wakeupThe condition variable provides wait/signal. The mutex protects the predicate. Together they enable safe waiting for shared state conditions. Neither is useful for this purpose without the other.
For condition variables to work correctly, several invariants must be maintained. Violating any of them leads to bugs ranging from data races to deadlocks.
Invariant 1: The mutex must be held when calling wait.
This is the most critical invariant. The wait operation releases the mutex and reacquires it. If you don't hold it initially:
12345678910111213
// WRONG: Wait without holding mutex void buggy_waiter(void) { // Forgot to lock mutex! pthread_cond_wait(&cond, &mutex); // UNDEFINED BEHAVIOR // Possible outcomes: // - Crash // - Deadlock // - Silent corruption of internal state // - Sometimes appears to work (worst case!)}Invariant 2: The same mutex must be used consistently with a condition variable.
Once you pair a mutex with a condition variable, all operations must use that pairing:
12345678910111213141516171819202122232425
// WRONG: Using different mutexes with the same condition variable pthread_mutex_t mutex1, mutex2;pthread_cond_t cond; void thread_a(void) { pthread_mutex_lock(&mutex1); while (!ready) { pthread_cond_wait(&cond, &mutex1); // Uses mutex1 } pthread_mutex_unlock(&mutex1);} void thread_b(void) { pthread_mutex_lock(&mutex2); while (!done) { pthread_cond_wait(&cond, &mutex2); // Uses mutex2 - BUG! } pthread_mutex_unlock(&mutex2);} // This is undefined behavior. POSIX says:// "The effect of using more than one mutex for concurrent// pthread_cond_wait() or pthread_cond_timedwait() operations// on the same condition variable is undefined."Invariant 3: The predicate must be protected by the same mutex.
The state checked in the while condition must be protected by the mutex passed to wait:
12345678910111213141516
// WRONG: Predicate protected by different mutex pthread_mutex_t mutex_a, mutex_b;pthread_cond_t cond;int shared_value; // Protected by mutex_b, not mutex_a! void broken(void) { pthread_mutex_lock(&mutex_a); while (shared_value == 0) { // Reads state protected by mutex_b! pthread_cond_wait(&cond, &mutex_a); // Releases mutex_a, not mutex_b } // This is a data race on shared_value pthread_mutex_unlock(&mutex_a);}| Invariant | Requirement | Consequence of Violation |
|---|---|---|
| Hold mutex on wait | Call wait() only while holding the mutex | Undefined behavior, crashes, deadlock |
| Consistent mutex | Always use same mutex with a given CV | Undefined behavior, lost wakeups |
| Predicate protection | Mutex must protect all predicate state | Data races, checking stale values |
| Hold mutex on check | Check predicate only while holding mutex | Race between check and wait |
The interaction between condition variables and mutexes during wait is a carefully choreographed dance. Let's trace through exactly what happens:
12345678910111213141516171819202122232425262728293031
// Detailed trace of mutex ownership during wait void waiter_detailed(void) { // T0: Thread holds NO locks pthread_mutex_lock(&mutex); // T1: Thread holds mutex while (!condition) { // T2: Still holds mutex, about to wait pthread_cond_wait(&cond, &mutex); // Inside wait(): // T3: Added to wait queue (still holds mutex momentarily) // T4: Mutex released atomically with entering wait queue // T5: Thread is blocked (holds NO locks) // ... time passes, other threads run ... // T6: Signaled! Thread is woken // T7: Thread tries to reacquire mutex (may block again) // T8: Mutex acquired! // T9: Returns from wait() // T10: Holds mutex again, will loop and recheck } // T11: Condition is TRUE, holds mutex do_work(); pthread_mutex_unlock(&mutex); // T12: Holds NO locks}Key observations:
At T5, the thread holds no locks. This is essential—if it held the mutex, no other thread could modify the condition.
T6-T8 involves two types of blocking. First, the thread blocks on the condition variable. Then, after being signaled, it may block again waiting for the mutex.
The mutex acquisition at T8 may not be immediate. Other threads might hold the mutex when the signaled thread wakes.
Between T6 and T10, the state may change. This is why while-loop checking is mandatory.
A thread calling cond_wait() may block twice: first waiting for the signal, then waiting for the mutex. This can be surprising to programmers who expect a 'signaled' thread to immediately proceed.
Not all mutexes are created equal. POSIX defines several mutex types, and your choice affects condition variable behavior.
PTHREAD_MUTEX_NORMAL (default):
The basic mutex type. Undefined behavior if locked twice by the same thread.
PTHREAD_MUTEX_RECURSIVE:
Can be locked multiple times by the same thread. Must be unlocked the same number of times. Works with condition variables, with caveats:
1234567891011121314151617181920212223242526272829303132
// Recursive mutex with condition variable - CAUTION REQUIRED pthread_mutexattr_t attr;pthread_mutexattr_init(&attr);pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_t rmutex;pthread_mutex_init(&rmutex, &attr); pthread_cond_t cond = PTHREAD_COND_INITIALIZER; void recursive_function(int depth) { pthread_mutex_lock(&rmutex); // Lock count = depth if (depth > 0) { recursive_function(depth - 1); } else { while (!ready) { // WARNING: cond_wait releases ALL lock levels // Not just one! This may surprise you. pthread_cond_wait(&cond, &rmutex); // On return, all levels are restored } } pthread_mutex_unlock(&rmutex);} // The behavior with recursive mutexes and CVs is:// cond_wait() releases the mutex fully (lock count -> 0)// On return, the full lock count is restored// Most implementations support this, but it's trickyPTHREAD_MUTEX_ERRORCHECK:
Returns an error (instead of undefined behavior) for programming errors like double-locking from the same thread. Good for debugging.
Reader-Writer Locks:
POSIX reader-writer locks (pthread_rwlock_t) do NOT work with standard condition variables. If you need CV-like semantics with reader-writer locks, you need a more complex design.
Unless you have a specific reason to use recursive mutexes, prefer normal mutexes with condition variables. The semantics are simpler and the code is easier to reason about. Recursive mutexes often indicate a design problem.
| Mutex Type | CV Compatible? | Behavior on Wait |
|---|---|---|
| PTHREAD_MUTEX_NORMAL | Yes | Standard semantics |
| PTHREAD_MUTEX_RECURSIVE | Yes* | *Fully releases, restores on return |
| PTHREAD_MUTEX_ERRORCHECK | Yes | Same as normal, with error checking |
| pthread_rwlock_t | No | Not supported with pthread_cond |
In POSIX threads, the association between a condition variable and its mutex is dynamic—you pass the mutex as a parameter to each wait call. Other languages use static associations.
POSIX (Dynamic Association):
12345678910
// POSIX: Mutex passed explicitly to each wait call pthread_mutex_t mutex;pthread_cond_t cond; // The association is made at each call sitepthread_cond_wait(&cond, &mutex); // Pair cond with mutex // Technically, you could use different mutexes (but shouldn't)// The relationship is not enforced by the type systemJava (Static Association with Lock):
123456789101112131415161718
// Java with Lock interface: Static association Lock lock = new ReentrantLock();Condition notEmpty = lock.newCondition(); // Created FROM lockCondition notFull = lock.newCondition(); // Also from same lock lock.lock();try { while (isEmpty()) { notEmpty.await(); // No lock parameter - it's implicit! }} finally { lock.unlock();} // The Condition is permanently tied to its Lock// You cannot await on notEmpty while holding a different lock// This prevents the "different mutex" bug at compile timeRust (Static Association with Type System):
12345678910111213141516171819
// Rust: Association enforced by ownership use std::sync::{Mutex, Condvar}; // The Condvar and data are bundled togetherlet pair = Arc::new((Mutex::new(false), Condvar::new())); let (lock, cvar) = &*pair; // To wait, you need the MutexGuard, which proves you hold the locklet mut guard = lock.lock().unwrap();while !*guard { guard = cvar.wait(guard).unwrap(); // Takes and returns guard} // The type system ensures:// 1. You must hold the lock to call wait// 2. You get the lock back after waiting// 3. Cannot mix up which lock goes with which condvarLet's examine the most common violations of the CV-mutex relationship and how to debug them.
Violation 1: Wait without holding mutex
12345678910111213141516171819202122232425
// BUG: Forgot to lock void buggy_wait(void) { while (!ready) { pthread_cond_wait(&cond, &mutex); // CRASH or worse }} // Symptoms:// - May crash immediately with assertion failure// - May appear to work (dangerous!)// - Helgrind/ThreadSanitizer will catch this // Detection:// mutex attribute type = PTHREAD_MUTEX_ERRORCHECK// Returns EPERM instead of undefined behavior // Fix:void fixed_wait(void) { pthread_mutex_lock(&mutex); // Add this! while (!ready) { pthread_cond_wait(&cond, &mutex); } pthread_mutex_unlock(&mutex);}Violation 2: Check predicate without holding mutex
1234567891011121314151617181920212223
// BUG: Data race on predicate check void buggy_check(void) { if (!ready) { // DATA RACE! No mutex held pthread_mutex_lock(&mutex); pthread_cond_wait(&cond, &mutex); pthread_mutex_unlock(&mutex); }} // Problem:// - 'ready' can change between check and wait// - Another thread sets ready=true and signals// - We miss the signal and wait forever // Fix:void fixed_check(void) { pthread_mutex_lock(&mutex); while (!ready) { // Check inside lock pthread_cond_wait(&cond, &mutex); } pthread_mutex_unlock(&mutex);}Violation 3: Wrong mutex with condition variable
12345678910111213141516171819202122232425262728
// BUG: Inconsistent mutex usage pthread_mutex_t mutex_a, mutex_b;pthread_cond_t cond;int count; // Protected by mutex_a void thread1(void) { pthread_mutex_lock(&mutex_a); while (count == 0) { pthread_cond_wait(&cond, &mutex_a); // Uses mutex_a } count--; pthread_mutex_unlock(&mutex_a);} void thread2(void) { pthread_mutex_lock(&mutex_b); // WRONG: uses mutex_b while (count == 0) { pthread_cond_wait(&cond, &mutex_b); // BUG: different mutex! } count--; // DATA RACE! pthread_mutex_unlock(&mutex_b);} // This is undefined behavior. May cause:// - Lost wakeups// - Data races on 'count'// - Strange deadlocksUse ThreadSanitizer (-fsanitize=thread) or Helgrind (Valgrind tool) to detect CV/mutex violations. These tools can catch incorrect mutex usage, data races, and other concurrency bugs. Enable them in your CI pipeline!
Good design makes the CV-mutex relationship obvious and hard to violate.
Pattern 1: Bundle CV, mutex, and state together
1234567891011121314151617181920212223242526272829303132
// Pattern: Encapsulate related items in a struct typedef struct { pthread_mutex_t mutex; pthread_cond_t not_empty; pthread_cond_t not_full; Item* items; int head, tail, count, capacity;} BoundedBuffer; void buffer_init(BoundedBuffer* b, int capacity) { pthread_mutex_init(&b->mutex, NULL); pthread_cond_init(&b->not_empty, NULL); pthread_cond_init(&b->not_full, NULL); b->items = malloc(capacity * sizeof(Item)); b->head = b->tail = b->count = 0; b->capacity = capacity;} void buffer_put(BoundedBuffer* b, Item item) { pthread_mutex_lock(&b->mutex); // Always use b->mutex while (b->count == b->capacity) { pthread_cond_wait(&b->not_full, &b->mutex); } b->items[b->tail] = item; b->tail = (b->tail + 1) % b->capacity; b->count++; pthread_cond_signal(&b->not_empty); pthread_mutex_unlock(&b->mutex);} // The bundling makes it clear which CV goes with which mutexPattern 2: Helper functions that enforce pairing
1234567891011121314151617181920212223242526272829303132333435
// Pattern: Hide mutex operations behind abstraction typedef struct Monitor { pthread_mutex_t mutex; pthread_cond_t cond;} Monitor; void monitor_wait(Monitor* m, bool (*predicate)(void*), void* arg) { pthread_mutex_lock(&m->mutex); while (!predicate(arg)) { pthread_cond_wait(&m->cond, &m->mutex); } // Note: returns WITH mutex held! // Caller must call monitor_leave()} void monitor_signal(Monitor* m) { pthread_cond_signal(&m->cond); // Assumes caller holds mutex} void monitor_leave(Monitor* m) { pthread_mutex_unlock(&m->mutex);} // Usage:bool is_ready(void* arg) { return *((bool*)arg);} void user_code(void) { monitor_wait(&m, is_ready, &ready); // Do work... monitor_leave(&m);}Pattern 3: RAII-style lock management (C++)
12345678910111213141516171819202122232425262728293031323334
// C++ RAII: Lock is acquired and released automatically #include <mutex>#include <condition_variable> class ProducerConsumer { std::mutex m; std::condition_variable cv; bool ready = false; public: void wait_for_ready() { std::unique_lock<std::mutex> lock(m); // Acquires mutex // Lambda makes predicate clear cv.wait(lock, [this]{ return ready; }); // Mutex is released automatically when 'lock' is destroyed } void signal_ready() { { std::lock_guard<std::mutex> lock(m); ready = true; } // Mutex released here cv.notify_one(); }}; // The unique_lock/lock_guard pattern makes it impossible to:// - Forget to unlock// - Call wait without holding lock (wait() checks lock.owns_lock())// - Leak lock on exceptionUnlike wait, signal and broadcast do not require the mutex as a parameter. In fact, POSIX permits calling them without holding the mutex at all. But this doesn't mean the mutex is irrelevant to signaling.
Why signal doesn't take a mutex parameter:
The signal operation only manipulates the condition variable's wait queue. It doesn't access your predicate or shared state. The mutex's role is to protect your state, not the raw signaling mechanism.
However:
You almost always should hold the mutex when signaling. Here's why:
12345678910111213141516171819202122232425262728293031
// Scenario: Why hold mutex when signaling // If you don't hold the mutex when signaling: void producer_no_lock(Item item) { // No lock acquired! buffer_add(item); // Where's the protection for buffer? pthread_cond_signal(¬_empty); // Legal, but dubious} // Problems:// 1. You needed to modify shared state (buffer) - race condition!// 2. State change and signal are not atomic // Correct pattern:void producer_with_lock(Item item) { pthread_mutex_lock(&mutex); buffer_add(item); // Safe: we hold mutex count++; // Safe: we hold mutex pthread_cond_signal(¬_empty); // Signal while locked pthread_mutex_unlock(&mutex);} // The mutex isn't logically required for signal(),// but it's required for modifying the state that// makes the signal meaningful!The "unlock then signal" debate revisited:
Some argue for signaling after unlocking for performance:
Signal while holding the mutex unless you have profiling data showing it matters AND you've carefully analyzed the race conditions. The simpler pattern is almost always correct and fast enough.
The relationship between condition variables and mutexes is fundamental to their correct use. Let's consolidate the key points:
The golden pattern:
pthread_mutex_lock(&mutex);
while (!predicate) {
pthread_cond_wait(&cond, &mutex);
}
// predicate is true, mutex is held
do_protected_work();
pthread_mutex_unlock(&mutex);
Every line of this pattern is essential. The lock protects the predicate check. The while loop handles spurious wakeups. The wait releases and reacquires. The unlock allows others to proceed.
What's next:
In the final page of this module, we'll explore multiple condition variables—how to use several CVs with the same mutex to enable more precise signaling and avoid wakeup issues.
You now understand the essential relationship between condition variables and mutexes. This understanding is crucial for writing correct concurrent code—the CV provides the wait/signal mechanism, the mutex protects the predicate. They are inseparable partners in synchronization.