1 Definition and purpose

A condition variable is a synchronization primitive that lets a thread pause execution until a shared-state condition changes. It is usually paired with a mutex or similar lock, which protects the data being inspected. Rather than representing a condition itself, the primitive provides a disciplined way for threads to wait and be notified when progress may be possible.

1.1 Basic concept

The basic idea is straightforward: one thread checks a condition on shared data, and if the condition is not yet satisfied, it waits. Another thread later updates the shared data and signals that waiting threads should recheck the condition. This design avoids wasteful busy-waiting and helps coordinate work among concurrent tasks.

1.2 Relationship to shared state

Condition variables are tied to shared state because the condition of interest usually depends on variables protected by a lock. Examples include whether a buffer is empty, whether a queue contains work, or whether a phase of computation has completed. The variable itself does not carry the state; the shared data structure does.

1.3 Role in thread synchronization

In thread synchronization, a condition variable acts as a rendezvous point between waiting and producing threads. It allows one thread to block until another thread makes a relevant state change. This makes it useful in systems where threads must proceed in a specific order or only after certain resources become available.

2 Operation

Condition variables follow a common pattern: check a predicate, wait if needed, and signal after changing state. The waiting thread releases the associated lock while sleeping and reacquires it before returning. This ensures that the shared data remains protected while the condition is tested and while it is acted upon.

2.1 Waiting

Waiting is the operation by which a thread suspends itself until notified. The thread first verifies that the desired predicate is false, then enters a blocked state. When awakened, it typically resumes by obtaining the lock again and rechecking the condition before continuing.

2.1.1 Releasing and reacquiring the lock

When a thread waits, it must release the mutex atomically with entering the wait state. This prevents a race in which another thread signals before the waiting thread is actually asleep. After notification, the waiting thread reacquires the lock before it proceeds, ensuring consistent access to the shared data.

2.1.2 Spurious wakeups

A waiting thread may sometimes wake up even without a corresponding signal. These spurious wakeups are permitted by many implementations and are part of the standard programming model. For that reason, code must never assume that a wakeup guarantees the condition has become true.

2.2 Signaling

Signaling informs waiting threads that the shared state may have changed. It does not usually guarantee that the condition is now true; it only indicates that waiting threads should try again. The precise effect depends on whether one thread or many are notified.

2.2.1 notify one thread

Notifying one thread wakes a single waiting thread, if any are present. This is often sufficient when only one waiter can make progress, such as when a single queue item is added. It can also reduce unnecessary contention compared with waking all waiters.

2.2.2 notify all threads

Notifying all threads wakes every thread waiting on the condition variable. This is useful when a state change may allow multiple waiters to proceed or when it is not known which thread can continue. However, it may also create a burst of contention as all awakened threads compete for the lock.

2.3 Predicate-based waiting

Correct use of condition variables centers on a predicate, meaning a boolean expression over shared state that determines whether a thread may continue. The thread waits only while the predicate is false. This makes the synchronization logic explicit and ties wakeups to meaningful state changes.

2.3.1 Checking conditions in a loop

Because wakeups can occur without the condition being satisfied, the predicate must be checked in a loop. Each time the thread wakes, it re-examines the shared state under the lock. This pattern ensures correctness even when notifications are early, redundant, or spurious.

3 Implementation details

Internally, condition variables are designed to coordinate with locks and kernel or runtime scheduling mechanisms. Their behavior is defined in terms of atomic transitions between checking, sleeping, and waking. Although the exact mechanics vary by platform, the same conceptual rules apply across most implementations.

3.1 Association with mutexes

Condition variables are normally used with a mutex that guards the associated shared data. The mutex ensures that the predicate is evaluated consistently and that state updates and notifications occur in a controlled order. Some systems allow more flexible usage, but the mutex-based pattern is the standard and safest approach.

3.2 Atomicity requirements

A key requirement is that unlocking the mutex and beginning to wait must happen atomically from the perspective of other threads. Without this guarantee, a notification could be lost between the check and the sleep. Atomicity prevents that gap and makes the wait-notify sequence reliable.

3.3 Wake-up semantics

Wake-up semantics describe what happens after a thread is notified. In most designs, notification merely marks the thread as eligible to run; it does not mean immediate execution. The awakened thread usually competes for the mutex and may run later depending on scheduler decisions.

3.4 Fairness and scheduling

Condition variables do not usually promise strict fairness. A particular thread may wait longer than others depending on scheduling, lock contention, and implementation details. As a result, applications should avoid relying on the order in which waiting threads wake unless the surrounding algorithm explicitly guarantees it.

4 Usage patterns

Condition variables appear in many coordination patterns where threads need to sleep until work becomes available or a phase changes. They are especially useful when the system must remain efficient under low activity. Common designs combine a shared queue, a predicate, and notification after each state update.

4.1 Producer-consumer problem

In the producer-consumer pattern, producers place items into a shared buffer and consumers remove them. A consumer waits while the buffer is empty, and a producer signals when new data arrives. This arrangement is one of the classic uses of condition variables.

4.2 Barrier-style coordination

Condition variables can help implement barrier-like coordination, where threads wait until all participants reach a certain point. Each thread records its arrival in shared state and then waits until the last one arrives or until a target count is reached. Once the barrier opens, waiting threads proceed together.

4.3 Task queues

Task queues often use condition variables to block worker threads when no tasks are available. When new tasks are submitted, one or more workers are notified. This avoids polling and allows thread pools to conserve CPU time during idle periods.

4.4 Event notification

A condition variable can also serve as a general event-notification mechanism. One thread changes a shared flag or status field, and other threads wait until that event is observed. The event is usually represented in shared memory rather than inside the condition variable itself.

5 Programming language support

Most major concurrency libraries provide condition variables or close equivalents. While the naming and exact API differ, the usage pattern is broadly similar: lock, test, wait, and notify. Language support typically includes both timed and untimed waiting operations.

5.1 POSIX threads

POSIX threads provide condition variables through the pthreads API. A thread waits with a condition-wait function that releases the mutex while sleeping and reacquires it afterward. Signaling functions are used to wake one or all waiting threads.

5.2 C++ standard library

C++ offers condition variables in its standard threading library. They are used with std::mutex and a predicate-based waiting style is commonly recommended. The language also provides timed waiting and convenience functions that combine waiting with predicate checks.

5.3 Java concurrency utilities

Java includes condition support through intrinsic monitors and explicit lock-based condition objects. These facilities are widely used in concurrent collections and custom synchronization classes. Java’s higher-level utilities often build upon the same waiting and notification principles.

5.4 Python threading

Python’s threading library includes condition objects for coordinating threads. A Python condition is typically associated with a lock and used with wait, notify, and notify_all operations. It is commonly applied in producer-consumer code and other thread communication tasks.

5.5 Other concurrency frameworks

Many other environments provide similar mechanisms, including systems for managed runtimes, embedded software, and event-driven frameworks. Some libraries expose condition variables directly, while others offer abstractions that internally use comparable waiting semantics. The core purpose remains the same: efficient coordination based on shared state.

6 Common pitfalls

Although condition variables are powerful, they are easy to use incorrectly. Most errors come from misunderstanding the relationship between state, locking, and notification. Careful attention to the predicate and lock discipline is essential for correct behavior.

6.1 Missed notifications

A missed notification can occur when a signal is sent before a thread begins waiting and the state change is not recorded in a shared predicate. This is why the condition must be stored in memory and checked under the lock. Proper predicate-based logic prevents wakeups from being lost.

6.2 Deadlocks

Deadlocks may arise if a thread waits while holding the wrong locks or if multiple locks are acquired in inconsistent order. Since condition-variable waits interact with mutexes, incorrect locking strategy can block progress across threads. Designing a clear lock hierarchy reduces this risk.

6.3 Race conditions

Race conditions occur when shared state is accessed without proper synchronization. If a thread checks a condition without holding the associated lock, another thread may change the state before the waiter sleeps. This can lead to incorrect assumptions, missed work, or unexpected blocking.

6.4 Incorrect predicate checks

A common mistake is to use if instead of a loop when waiting. Because of spurious wakeups and state changes by other threads, a single check is insufficient. The predicate must be re-evaluated after every wakeup to ensure the required condition truly holds.

Condition variables are often discussed alongside other synchronization tools, but each serves a different purpose. Some are designed mainly for exclusion, others for counting or one-time notification. Choosing the right primitive depends on the coordination problem being solved.

7.1 Semaphores

Semaphores track availability through an internal count, which can be incremented and decremented by threads. Unlike condition variables, semaphores remember signals even if no thread is currently waiting. This makes them useful in some resource-counting scenarios, though their semantics differ from predicate-based waiting.

7.2 Mutexes and monitors

Mutexes provide mutual exclusion, not waiting for a state change. Monitors combine mutual exclusion with waiting and notification in a higher-level structured form. Condition variables are often the waiting component inside monitor-style designs.

7.3 Futures and promises

Futures and promises represent a value or result that will become available later. They are often used for one-time completion rather than repeated coordination on shared state. Condition variables are more general when multiple state transitions and many waiters must be managed.

7.4 Events and signals

Events and signals are notification mechanisms that may resemble condition variables in use. Some are one-shot, while others can be reset or persisted until observed. Condition variables differ in that they are typically paired with a lock and a predicate over shared data, making them especially suited to fine-grained thread coordination.

</INTERNAL_LINK_CANDIDATES> Mutex (a lock used to protect shared state during access) Semaphore (a counter-based synchronization primitive for resource control) Monitor (a structured concurrency construct combining locking and waiting) Spurious wakeup (a wakeup from waiting without an explicit signal) Predicate (a boolean condition checked to decide whether waiting should continue) Producer-consumer problem (a coordination pattern between item-producing and item-consuming threads) Task queue (a shared queue of work items processed by worker threads) Thread pool (a set of reusable worker threads that execute queued tasks) Barrier (a synchronization point where threads wait for one another) Deadlock (a state where threads block indefinitely waiting on each other) Race condition (an error caused by unsynchronized access to shared data) Shared state (data accessed by multiple threads and protected by synchronization) Notification (a signal that waiting threads should recheck a condition) Atomicity (an all-or-nothing operation without observable intermediate states) Lock contention (competition among threads for the same lock) Timed waiting (waiting that ends after a specified duration if not notified) Monitor-style design (a concurrency pattern built around a lock and condition variable) Wakeup semantics (the rules governing what happens when a waiting thread is notified) Reacquire (to obtain a lock again after returning from a wait) Concurrency library (software support for synchronization and threading)