How std::condition_variable Coordinates C++ Threads

In modern C++, std::condition_variable is a synchronization primitive that enables multiple threads to communicate by waiting for and signaling specific state changes. This article explains the coordination mechanism of std::condition_variable, how it interacts with std::unique_lock to prevent busy-waiting and race conditions, how notifications operate, and how to handle spurious wakeups effectively.

The Purpose of std::condition_variable

When multiple threads share data, one thread often needs to pause execution until another thread modifies the shared state. Without a condition variable, a waiting thread must repeatedly poll the state inside a loop, consuming significant CPU resources (known as busy-waiting).

std::condition_variable eliminates busy-waiting by allowing a thread to put itself to sleep while waiting for a condition to become true. The operating system will only wake the sleeping thread when another thread explicitly signals that the state has changed.

The Coordination Workflow

Thread coordination using std::condition_variable relies on three components:

  1. A boolean condition representing the shared state.
  2. A std::mutex that protects that shared state.
  3. A std::condition_variable instance that manages the sleep and wake signals.

The coordination process follows these distinct phases:

1. Waiting on a Condition

To wait, a consumer thread acquires ownership of the mutex using a std::unique_lock<std::mutex>:

std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return ready; });

The wait() function performs three actions automatically:

  1. It evaluates the predicate (e.g., ready). If true, execution continues immediately without blocking.
  2. If false, it atomically releases the mutex and suspends the calling thread into a blocked state.
  3. When the thread is notified and wakes up, it reacquires the mutex before checking the predicate again.

A std::unique_lock is strictly required instead of std::lock_guard because wait() must be able to unlock and re-lock the mutex dynamically.

2. Modifying State and Notifying

When a producer thread updates the shared state, it follows these steps:

{
    std::lock_guard<std::mutex> lock(mtx);
    ready = true; // Modify the shared state
}
cv.notify_one(); // or cv.notify_all();
  1. The producer acquires the mutex to ensure exclusive access to the shared state.
  2. It modifies the shared state (setting ready = true).
  3. It releases the mutex (either implicitly at scope exit or explicitly).
  4. It calls notify_one() to wake up a single waiting thread, or notify_all() to wake all waiting threads.

Signaling after releasing the lock avoids a scenario called "pessimization," where a signaled thread wakes up only to immediately block again waiting for the producer to release the mutex.

Handling Spurious Wakeups

A waiting thread can occasionally wake up even if no thread has called notify_one() or notify_all(). This phenomenon is known as a spurious wakeup and is caused by underlying operating system kernel behavior.

Because of spurious wakeups, a condition variable must never be used without verifying the underlying condition. Modern C++ addresses this by providing a two-argument overload of wait() that takes a predicate function:

cv.wait(lock, [] { return is_ready; });

This overload is equivalent to:

while (!is_ready) {
    cv.wait(lock);
}

The loop ensures that if the thread wakes up spuriously and is_ready is still false, it automatically re-enters the waiting state.

Choosing Between notify_one() and notify_all()

  • notify_one(): Unblocks one of the threads currently waiting on the condition variable. If multiple threads are waiting, the OS scheduler decides which one wakes up. This is ideal for work-queue models where only one worker should process each newly available task.
  • notify_all(): Unblocks all waiting threads. This is necessary when multiple threads are waiting for a global event, such as a shutdown signal, or when the state change could satisfy conditions for multiple distinct threads.