Skip to content

Condition Variables & Producer-Consumer

Use condition variables to efficiently coordinate threads. Build a thread-safe producer-consumer queue, handle spurious wakeups, and avoid the lost wakeup problem.

Thread Coordination with Condition Variables

A condition variable lets a thread wait until some condition becomes true, without busy-looping. Instead of spinning while (!ready) { /* burn CPU */ }, the thread sleeps and is woken up by another thread that calls notify_one() or notify_all().

std::condition_variable works only with std::unique_lock. The pattern is always:

1. Lock the mutex.
2. Check the condition — if false, call wait() which atomically releases the mutex and suspends the thread.
3. When notified (and the condition is true), the mutex is re-acquired and execution continues.

The notifying thread modifies the shared state under the same mutex, then calls notify_one() or notify_all().

Basic Condition Variable Usage

Here is the fundamental pattern: one thread waits for data, another provides it.

condition_variable_basic.cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <string>

std::mutex mtx;
std::condition_variable cv;
std::string data;
bool ready = false;

void consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    // Wait until ready is true (handles spurious wakeups)
    cv.wait(lock, []{ return ready; });
    std::cout << "Consumer received: " << data << "\n";
}

void producer() {
    {
        std::lock_guard<std::mutex> lock(mtx);
        data = "Hello from producer";
        ready = true;
    }  // unlock before notify to avoid waking thread into contention
    cv.notify_one();
}

int main() {
    std::thread t1(consumer);
    std::thread t2(producer);
    t1.join();
    t2.join();
    return 0;
}
Pitfall

A condition variable may wake up a thread even when no notification was sent — this is called a spurious wakeup. It is allowed by the C++ standard (and common on real systems) for performance reasons. You must always use the predicate form of wait() or wrap it in a while loop:

```cpp
// CORRECT — predicate form handles spurious wakeups
cv.wait(lock, []{ return ready; });

// EQUIVALENT manual loop
while (!ready) {
cv.wait(lock);
}

// WRONG — wakes up randomly, proceeds without data
cv.wait(lock); // no condition check!
```

Never call cv.wait(lock) without a predicate or a surrounding while loop.

Thread-Safe Bounded Buffer (Producer-Consumer)

The producer-consumer pattern is one of the most common concurrency patterns. Producers add items to a shared buffer; consumers remove them. A bounded buffer blocks producers when full and consumers when empty.

bounded_queue.cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <vector>

template <typename T>
class BoundedQueue {
    std::queue<T> queue_;
    std::mutex mtx_;
    std::condition_variable not_full_;
    std::condition_variable not_empty_;
    size_t capacity_;
public:
    explicit BoundedQueue(size_t cap) : capacity_(cap) {}

    void push(T item) {
        std::unique_lock<std::mutex> lock(mtx_);
        not_full_.wait(lock, [this]{ return queue_.size() < capacity_; });
        queue_.push(std::move(item));
        not_empty_.notify_one();
    }

    T pop() {
        std::unique_lock<std::mutex> lock(mtx_);
        not_empty_.wait(lock, [this]{ return !queue_.empty(); });
        T item = std::move(queue_.front());
        queue_.pop();
        not_full_.notify_one();
        return item;
    }
};

int main() {
    BoundedQueue<int> bq(5);  // capacity 5

    // Producer
    std::thread producer([&bq]() {
        for (int i = 0; i < 20; ++i) {
            bq.push(i);
            std::cout << "Produced: " << i << "\n";
        }
    });

    // Consumer
    std::thread consumer([&bq]() {
        for (int i = 0; i < 20; ++i) {
            int val = bq.pop();
            std::cout << "Consumed: " << val << "\n";
        }
    });

    producer.join();
    consumer.join();
    return 0;
}

The Lost Wakeup Problem

A lost wakeup occurs when notify_one() is called before the consumer calls wait(). Since no thread is waiting, the notification is lost — the consumer then sleeps forever.

The standard pattern prevents this by checking the predicate before sleeping. If the producer already set the condition to true, the predicate returns true and wait() returns immediately without sleeping. This is why the predicate form cv.wait(lock, pred) is essential — it checks the condition before and after every wakeup:

1. Lock the mutex.
2. Check the predicate — if already true, skip waiting.
3. Otherwise, atomically unlock and sleep.
4. On wakeup, re-lock and re-check the predicate.

As long as both producer and consumer protect the shared flag with the same mutex, no wakeup can be lost.

notify_one vs notify_all

notify_one() wakes one waiting thread; notify_all() wakes all of them. Use notify_all() when multiple threads may need to re-check their conditions (e.g., shutdown signals), and notify_one() when exactly one thread should proceed (e.g., a single consumer picking up a task).

notify_all_shutdown.cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <vector>
#include <atomic>

std::mutex mtx;
std::condition_variable cv;
bool shutdown = false;

void worker(int id) {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return shutdown; });
    std::cout << "Worker " << id << " shutting down\n";
}

int main() {
    std::vector<std::thread> workers;
    for (int i = 0; i < 5; ++i) {
        workers.emplace_back(worker, i);
    }

    std::this_thread::sleep_for(std::chrono::seconds(1));

    {
        std::lock_guard<std::mutex> lock(mtx);
        shutdown = true;
    }
    cv.notify_all();  // wake ALL workers for shutdown

    for (auto& w : workers) w.join();
    return 0;
}
Best Practice

Always use the predicate overload of wait() to guard against spurious wakeups and lost wakeups. Unlock the mutex before calling notify_one()/notify_all() when possible — this avoids immediately blocking the woken thread on the mutex. Prefer notify_one() when only one thread needs to wake up, and notify_all() for broadcast scenarios like shutdown. Never wait on a condition variable with a lock_guard — you need unique_lock.

Key Takeaways
  • std::condition_variable lets threads sleep until a condition is true, avoiding expensive busy-waiting
  • Always use the predicate form of wait() to handle both spurious wakeups and lost wakeups
  • The producer-consumer pattern uses two condition variables: "not full" and "not empty"
  • notify_one() wakes one waiter; notify_all() wakes all waiters — use the right one for your scenario
  • Both the waiter and the notifier must use the same mutex to protect the shared condition

Quiz — Test Your Knowledge

(20 XP)

1. Why must you always use a predicate with `condition_variable::wait()`?

2. In a bounded producer-consumer queue, what should the producer do when the queue is full?

3. Which lock type does `std::condition_variable::wait()` require?