Race Conditions in Shared Data
A condition variable is a mechanism that allows one thread to wait for another thread.
Why Threads Sometimes Have to Wait
Many programs split their work across several threads, and those threads often depend on one another. One thread may be unable to continue until something happens somewhere else: a piece of data gets produced, a task finishes, or a particular state is reached. The central question of this article is how a thread can pause and wait for such an event without simply spinning in a loop and checking over and over whether the event has happened yet.
Keep this question in mind as you read: when a thread waits, what exactly is it waiting for, and who makes that thing happen? The answer is the key to the whole mechanism.
What do you think happens?
Suppose a consumer thread needs data that a producer thread has not created yet. While the consumer is waiting, which thread should be doing useful work?
Reveal answer
The producer, which is performing the work that creates the data
The waiting thread is parked while the other thread performs its work. The waiting thread's progress depends on that work, not on repeatedly asking whether it is finished.
What a Condition Variable Does
A condition variable is a mechanism that allows one thread to wait for another thread.
A waiting thread waits for a condition, often called an event, that occurs as the result of work done by other threads. The condition is a fact about the program's state, such as whether data is available. The condition variable is the channel through which the waiting thread learns that this fact has become true.
Notice the direction of the relationship. The waiting thread does not produce the condition itself. It depends on some other thread doing its job, and the condition variable is the tool that lets the waiting thread rest until that job has had its effect. Support for this kind of waiting is provided by condition variables, which live in the standard header named condition_variable.
The waiting thread's progress is tied to an event in another thread, not to a timer and not to repeatedly asking whether the event has happened yet.
Tracing the Handoff Step by Step
Before reading the mechanics in detail, follow the sequence of events in the diagram below. At each step, ask which thread is active and which thread is suspended waiting. The essential shape is simple: the waiting thread is parked while the other thread performs its work, and only after the condition has occurred does the waiting thread proceed.
Read the diagram as a timeline. Between steps 1 and 3 the consumer is not doing useful work; it is parked. The producer is the active thread, and its job of adding items is what brings the condition about. Only at step 4 does the consumer proceed, and that progress was caused by the producer's action rather than by a timer.
What do you think happens?
In the diagram, which thread's progress is blocked between wait() and notify()?
Reveal answer
The consumer thread
The consumer is the waiting thread. It is parked until the condition occurs, while the producer continues performing the work that makes the condition true.
A Shared Queue as the Channel
The source offers a classical illustration of this idea: two threads communicating by passing messages through a queue. One thread places items into the queue and another thread takes items out. The consumer's need for data is the condition it waits on, and the producer's work of adding items is what brings that condition about.
The data path and the coordination path are different. The items travel through the queue from producer to consumer. The condition variable sits beside that path: it carries the signal that tells the consumer there is something to take, so the consumer does not have to keep polling the queue.
Following one item through the queue
A consumer arrives at the queue when it is empty. A producer later adds one item. Trace what each thread does.
- 1Consumer needs data
The queue is empty, so the consumer's need for data is not yet met. This unmet need is the condition it waits on.
- 2Consumer waits
The consumer calls wait on the condition variable and is parked. It does not repeatedly check the queue in a loop.
- 3Producer adds an item
The producer places an item into the shared queue. Its work is what brings the condition about.
- 4Producer notifies
The producer signals through the condition variable that the event has occurred.
- 5Consumer resumes and takes the item
The consumer wakes, proceeds, and takes the item out of the queue.
The consumer's progress depended entirely on the producer's action, and the condition variable carried that event from one thread to the other.
Why Shared Queues Are Tricky to Get Right
Condition-variable-based sharing is powerful and efficient, and the source says it supports many elegant forms of cooperation. It also warns that the approach can be rather tricky to get right. Part of the difficulty is that the queue itself is a shared object. Both threads touch it, so the queue must be protected, and the source notes that the mechanism for avoiding race conditions on that queue is part of the same picture.
In the source's version of the example, the queue and that protective mechanism are declared globally so that both the producer and the consumer can reach them. Because both threads reach the same objects, the two threads' actions have to be arranged so they do not interfere with each other.
Common mistake Thinking the waiting thread produces the condition
Why it failsThe waiting thread depends on some other thread doing its job. The condition is a fact about program state that another thread makes true.
Do this insteadAsk who performs the work that makes the condition true. That thread is the one whose action wakes the waiter.
Common mistake Treating wait as a timer or a polling loop
Why it failsThe source describes waiting as tied to an event in another thread, not to a timer or to repeatedly checking whether the event has happened.
Do this insteadLink each wait to a specific event produced by another thread.
Common mistake Forgetting that the shared queue is touched by both threads at the same time
Common mistake Assuming the coordination mechanism alone makes the shared data safe
Practice Tracing a Handoff
Take a producer and a consumer that share a queue. The consumer reaches the queue before any item has been added. Write, in order, each action the consumer takes and each action the producer takes, marking which thread is active and which is parked at every step. Then identify the single event that lets the consumer continue.
Hints
- Start by asking what the consumer is waiting for, using the wording of the condition.
- Check whether the consumer is parked or active between its wait and the producer's notification.
- The event that ends the wait is the producer's action, not a timer.
Key Takeaways
- A condition variable is a mechanism that allows one thread to wait for another thread.
- A waiting thread waits for a condition that occurs as the result of work done by other threads, and it is parked rather than spinning in a loop.
- A shared queue between a producer and a consumer is a classic example: the producer's work of adding items brings about the condition the consumer waits on.
- Condition-variable-based sharing is powerful and efficient, but it can be rather tricky, largely because the shared queue must be protected from both threads touching it at once.