C++ PROGRAMMING ยท 13 MIN STUDY

Condition variables and producer-consumer synchronization

Coordinate threads that wait for shared state using condition_variable, mutexes, and predicate-based waits.

Learning objectives

  • Explain why a condition variable waits for a predicate rather than storing a notification.
  • Use std::unique_lock with predicate-based wait.
  • Protect the shared queue and shutdown state with the same mutex.
  • Describe how a worker should stop safely when no more jobs will arrive.

Prerequisites: std::thread, mutexes, lock guards, and shared-state data races. Examples require C++11 or later.

Why a condition variable?

A thread sometimes needs to wait until a condition becomes true, such as a queue becoming non-empty. Repeatedly checking in a loop wastes CPU time. A condition variable lets a thread sleep until another thread notifies it that shared state may have changed.

Producer-consumer example

Complete C++11 example: one producer places three jobs in the queue. One consumer waits for work, processes every queued job, and exits only after the producer marks the queue finished.

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

std::mutex m;
std::condition_variable ready;
std::queue<int> jobs;
bool finished = false;

void producer() {
    for (int job : {10, 20, 30}) {
        {
            std::lock_guard<std::mutex> lock(m);
            jobs.push(job);
        }
        ready.notify_one();
    }
    {
        std::lock_guard<std::mutex> lock(m);
        finished = true;
    }
    ready.notify_all();
}

void consumer() {
    while (true) {
        int job;
        {
            std::unique_lock<std::mutex> lock(m);
            ready.wait(lock, [] { return !jobs.empty() || finished; });
            if (jobs.empty()) return; // finished and no work remains
            job = jobs.front();
            jobs.pop();
        } // release the mutex before processing the job
        std::cout << "Processed " << job << '\n';
    }
}

int main() {
    std::thread worker(consumer);
    std::thread source(producer);
    source.join();
    worker.join();
}

The queue and finished flag are always read or changed while holding m. The consumer releases the lock before doing its work, so the producer can continue adding jobs. The predicate handles both spurious wakeups and notifications that occur before the consumer begins waiting. This example uses one consumer; with several consumers, notify workers as jobs arrive and notify all during shutdown so every waiting worker can re-check the finished state.

Build note: on many GCC and Clang Linux toolchains, compile this C++11 example with thread support enabled: g++ -std=c++11 -pthread producer_consumer.cpp -o producer_consumer. Other toolchains have their own thread-linking setup.

Why wait with a predicate?

Condition-variable waits can wake spuriously, and notifications can happen before a thread starts waiting. The predicate form repeatedly checks the condition while holding the mutex and sleeps only while it is false. The shared condition must be checked under the same mutex that protects its data.

unique_lock versus lock_guard

lock_guard holds a lock for a simple scope. unique_lock can be unlocked and relocked and is required by condition_variable::wait because the wait operation temporarily releases the mutex while sleeping, then locks it again before returning.

Common mistakes

  • Waiting without a predicate.
  • Changing the condition without holding the associated mutex.
  • Assuming notify_one stores a notification for a future waiter.
  • Holding the mutex while performing slow work unrelated to shared state.
  • Failing to define a shutdown condition for worker threads.

Practice

  1. Why can a condition-variable wait wake without a notification?
  2. Why does wait need a unique_lock?
  3. What shared data should the mutex protect in the example?
Show answers
  1. Spurious wakeups are permitted.
  2. It must release the mutex while waiting and reacquire it before returning.
  3. The queue and finished flag, because they form the predicate state.

Summary

Condition variables coordinate waiting threads around shared state. Protect the predicate with a mutex, always use predicate-based waits, and design shutdown explicitly.

References