C++ / a working model

73 / 163   ·   C++11   ·   11 min

Condition variables: predicates, lost wakeups, and spurious wakeups

Keep this sentence

A condition variable only lets a thread wait and check again; it does not store the event. Keep the real condition in shared state, modify and inspect it under the same mutex, and wait with a predicate. Then an early notification or a spurious wakeup cannot break the logic.

In this lesson
  1. Wait for state, not for a count of notifications
  2. wait atomically unlocks, then rechecks the predicate
  3. Write the close protocol into the normal control flow
  4. Example
  5. Exercise

Wait for state, not for a count of notifications

condition_variable is not a counting semaphore. It does not queue a notification that nobody is waiting to receive. If the consumer performs one unpredicated wait, the producer may notify before that wait begins. The consumer then sleeps, and nobody wakes it. Store the fact that work may continue as a nonempty queue, a completed task, or a closed flag.

The consumer first acquires the mutex and inspects the state. If the condition already holds, it does not sleep. The producer updates the state under the same lock and then notifies. Even when the notification arrived early, the state remains. The example predicate is closed || !queue.empty(): it covers both receiving data and exiting after production has finished.

wait atomically unlocks, then rechecks the predicate

wait(lock, pred) evaluates the predicate while the lock is held and waits only if it is false. The wait releases the mutex and enters the waiting state as one atomic step. After a wakeup it reacquires the lock and evaluates the predicate again. A producer therefore cannot slip into the gap between "I saw false" and "I have started waiting". A standard condition variable uses unique_lock<mutex> because the wait must release the lock for a while.

A wait may wake spuriously. Several consumers may also compete for the same item: being awakened does not mean data is still there when this thread runs. Rechecking the predicate handles both cases. A single if is not a substitute for the loop. Making ready atomic does not close the notification window between a condition check and a sleep. You still need the full waiting protocol.

Write the close protocol into the normal control flow

The example has one producer and one consumer. After submitting three integers, the producer sets closed under the lock and notifies. Once awakened, the consumer drains the queue first and exits only when the queue is empty. Because the predicate already holds, an empty queue then means closed. The main thread joins last, so the condition variable and mutex are unused before they are destroyed.

Notifying after unlocking can reduce the chance that a just-awakened waiter immediately contends for the lock. That is not required for correctness, provided the condition variable is still alive. A new item can use notify_one. Closing should use notify_all so every waiter rechecks the exit condition.

If you need a timeout, use an absolute deadline. Repeatedly waiting for a full remaining duration in a loop can stretch the total timeout. A timeout return also does not mean the condition now holds.

Pitfalls

  • Do not treat notify_one as a voucher saved for the next consumer; the queue or a count is what stores how much work exists.
  • Do not join the producer while holding the mutex the consumer needs, and do not destroy the condition variable while a waiting thread may still access it.

Run an example

Minimum C++11 · complete program · Download .cpp

#include <cassert>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>

int main() {
    std::mutex mutex;
    std::condition_variable changed;
    std::queue<int> queue;
    bool closed = false;
    std::thread producer([&] {
        for (int value = 1; value <= 3; ++value) {
            {
                std::lock_guard<std::mutex> lock(mutex);
                queue.push(value);
            }
            changed.notify_one();
        }
        {
            std::lock_guard<std::mutex> lock(mutex);
            closed = true;
        }
        changed.notify_all();
    });

    int sum = 0;
    for (;;) {
        std::unique_lock<std::mutex> lock(mutex);
        changed.wait(lock, [&] { return closed || !queue.empty(); });
        if (queue.empty()) break;
        const int value = queue.front();
        queue.pop();
        lock.unlock();
        sum += value;
    }
    producer.join();
    assert(sum == 6);
    assert(queue.empty() && closed);
    std::cout << sum << '\n';
}

Compile locally

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread concurrency-condition-variable.cpp -o example && ./example

Expected result

6

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

If the producer submits three elements and closes before the consumer's first wait, does the consumer block forever? What happens if the wait is only wait(lock)?

Show a reference answer

The predicated version does not block: the first check already sees closed as true, takes the existing elements one by one, and exits when the queue is empty. Whether earlier notifications were received does not matter. The unpredicated version does not read the saved state and waits directly. Every notification may already have finished, so it can block forever. The fix is to restore the predicate, not to add sleep so the producer runs later.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog