Skip to content
C++
Library
since C++11
Intermediate

Condition Variables

std::condition_variable — atomic wait/notify, spurious wakeup handling, timed waits, condition_variable_any, and C++20 stop_token integration.

std::condition_variablesince C++11

A synchronization primitive that atomically releases a mutex and suspends the calling thread, resuming only when another thread calls notify while the associated predicate is true.

Overview

std::condition_variable solves a problem that a mutex alone cannot: blocking until some shared state changes without busy-polling. The critical property is that wait() performs an atomic unlock-and-sleep — the mutex is released and the thread is suspended as one indivisible operation. This closes the race between the condition check and the sleep:

cpp
Thread A (waiter):              Thread B (notifier):
  lock mutex
  check condition → false
  [atomic] unlock + suspend ←── lock mutex
                                 set condition = true
                                 unlock mutex
                                 notify_one()
  resume, re-acquire lock
  condition is now true ✓

Without this atomicity, a notification could arrive after the check but before the sleep, and be permanently lost. std::condition_variable pairs exclusively with std::unique_lock<std::mutex> (C++11). For any other lock type, use std::condition_variable_any (C++11) at a small additional overhead.

Syntax

cpp
#include <condition_variable>

// Wait forms — always prefer the predicate overload
void wait(std::unique_lock<std::mutex>& lock);
void wait(std::unique_lock<std::mutex>& lock, Predicate pred);

// Timed waits — return true if predicate became true, false on timeout
template<class Rep, class Period>
bool wait_for(std::unique_lock<std::mutex>& lock,
              const std::chrono::duration<Rep, Period>& rel_time,
              Predicate pred);

template<class Clock, class Duration>
bool wait_until(std::unique_lock<std::mutex>& lock,
                const std::chrono::time_point<Clock, Duration>& abs_time,
                Predicate pred);

// Notification — both are noexcept
void notify_one() noexcept;
void notify_all() noexcept;

The predicate overload of wait is exactly equivalent to:

cpp
while (!pred()) cv.wait(lock);

The loop is essential: spurious wakeups (thread resumes for OS-level reasons unrelated to your condition) are real on every mainstream platform. The bare wait(lock) form must never be used directly.

Examples

Basic wait/notify pattern

cpp
std::mutex              mtx;
std::condition_variable cv;
bool                    ready = false;

void consumer() {
    std::unique_lock lock{mtx};
    cv.wait(lock, [&]{ return ready; });  // re-checks after every wakeup
    // mutex is re-held; ready is guaranteed true
}

void producer() {
    {
        std::lock_guard g{mtx};
        ready = true;       // always modify shared state under lock
    }                       // release lock before notifying (see Best Practices)
    cv.notify_one();
}

Bounded producer-consumer queue (C++11)

Two separate condition variables prevent producers and consumers from waking each other unnecessarily:

cpp
template<typename T>
class BoundedQueue {
    std::queue<T>           data_;
    mutable std::mutex      mtx_;
    std::condition_variable not_empty_;
    std::condition_variable not_full_;
    const std::size_t       cap_;

public:
    explicit BoundedQueue(std::size_t cap) : cap_{cap} {}

    void push(T item) {
        std::unique_lock lock{mtx_};
        not_full_.wait(lock, [&]{ return data_.size() < cap_; });
        data_.push(std::move(item));
        lock.unlock();
        not_empty_.notify_one();  // exactly one consumer can take this item
    }

    T pop() {
        std::unique_lock lock{mtx_};
        not_empty_.wait(lock, [&]{ return !data_.empty(); });
        T item = std::move(data_.front());
        data_.pop();
        lock.unlock();
        not_full_.notify_one();   // exactly one producer slot freed
        return item;
    }

    // C++17: std::optional
    std::optional<T> try_pop() {
        std::lock_guard g{mtx_};
        if (data_.empty()) return std::nullopt;
        T item = std::move(data_.front());
        data_.pop();
        return item;
    }
};

Timed wait (C++11)

cpp
using namespace std::chrono_literals;

std::unique_lock lock{mtx};

// wait_for: relative timeout
bool ok = cv.wait_for(lock, 200ms, [&]{ return ready; });
if (!ok) { /* timed out — predicate is still false, lock re-held */ }

// wait_until: absolute deadline (prefer steady_clock for timeouts)
auto deadline = std::chrono::steady_clock::now() + 5s;
bool signaled = cv.wait_until(lock, deadline, [&]{ return ready; });

Always capture the return value. Silently discarding the bool leads to processing data that was never actually set.

Cancellable wait with stop_token (C++20)

std::condition_variable_any gained a three-argument wait overload in C++20 that accepts a std::stop_token. The wait returns (returning false) if a stop is requested, with no additional flag or extra condition variable required:

cpp
// C++20
std::condition_variable_any cv_any;
std::mutex                  mtx;
bool                        data_ready = false;

void worker(std::stop_token stoken) {
    std::unique_lock lock{mtx};
    // Wakes if data_ready == true OR stop is requested
    bool ok = cv_any.wait(lock, stoken, [&]{ return data_ready; });
    if (!ok) return;  // stop was requested
    // process data...
}

// From any other thread:
jthread t{worker};    // std::jthread (C++20) owns a stop_source
t.request_stop();     // wakes worker immediately, waiter returns false

This is the correct way to write cancellable blocking operations in C++20. The overload handles the stop-token registration internally and is free of the check-then-sleep race.

Best Practices

Notify outside the lock. Notifying while holding the mutex is correct but wasteful: the woken thread immediately re-blocks trying to re-acquire the lock it just lost. Release first, then notify:

cpp
// Preferred
{ std::lock_guard g{mtx}; state = true; }
cv.notify_one();

// Suboptimal — woken thread blocks immediately on mutex contention
{ std::lock_guard g{mtx}; state = true; cv.notify_one(); }

Choose notify_one vs notify_all deliberately.

  • notify_one — exactly one event produced, exactly one thread should handle it (task queue, producer-consumer). All others stay asleep.
  • notify_all — every waiting thread must react: shutdown signals, barrier releases, configuration broadcasts. If N threads wait on the same condition but only one can proceed per event, notify_all causes a thundering herd where N-1 threads wake, find the condition false, and re-block.

Use condition_variable_any + stop_token (C++20) for cancellable waits. The three-argument overload is race-free by construction; a manual stop flag with a second CV is error-prone and more code.

Keep predicates free of side effects. A predicate may be evaluated on entry to wait before the thread ever sleeps, and again after each wakeup. Predicates with side effects will trigger them multiple times.

Common Pitfalls

Lost notification. Notifications are not buffered. If notify_one() fires before the waiter reaches wait(), the event is gone forever:

cpp
// Thread A (runs first)
state = true;
cv.notify_one();    // no one is waiting yet — lost

// Thread B (runs after)
cv.wait(lock, [&]{ return state; });  // returns immediately because state==true

This is why you must always set shared state before calling notify, and why the predicate form is safe here: wait evaluates the predicate before sleeping and returns immediately if it is already true.

Bare wait without a predicate. Susceptible to spurious wakeups on every POSIX-compliant platform. Without a predicate, there is no way to distinguish a real notification from an OS-level spurious wake.

Modifying condition variables without the associated lock.

cpp
ready = true;      // data race — undefined behaviour
cv.notify_one();

Every variable read in the predicate must be written under the mutex. The predicate itself runs with the lock held.

Using lock_guard with wait.

cpp
std::lock_guard g{mtx};
cv.wait(g, pred);  // compile error: wait requires unique_lock

wait calls lock.unlock() internally; lock_guard provides no unlock() method.

Ignoring the timed-wait return value.

cpp
cv.wait_for(lock, 100ms, pred);  // discarded bool — was it a timeout?
// code below assumes condition is true — may not be

See Also

  • std::mutex — the required associated lock
  • std::unique_lock — the unlock-capable wrapper wait() requires
  • std::semaphore — C++20; prefer over a CV when the count itself is the condition
  • std::atomic — C++20 atomic::wait() / notify_one() for single-variable conditions without a mutex
  • std::latch / std::barrier — C++20 one-shot and cyclic rendezvous points; cleaner than a CV for fixed-count coordination
  • std::stop_token — C++20 cooperative cancellation mechanism