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

condition_variable_any and stop_token (C++20)

std::condition_variable_any generalises std::condition_variable to work with any lockable type, and in C++20 gains a three-argument wait overload that accepts a std::stop_token for interruptible waits. When stop_token.stop_requested() becomes true, wait unblocks and returns false — no manual flag, no second condition variable, and no check-then-sleep race.

cpp
// C++20
#include <condition_variable>
#include <mutex>
#include <deque>
#include <thread>
#include <optional>

template<typename T>
class CancellableQueue {
    std::deque<T>              items_;
    std::mutex                 mtx_;
    std::condition_variable_any cv_;

public:
    void push(T item) {
        {
            std::lock_guard g{mtx_};
            items_.push_back(std::move(item));
        }
        cv_.notify_one();
    }

    // Returns the item, or nullopt if stop was requested
    std::optional<T> pop(std::stop_token stoken) {
        std::unique_lock lock{mtx_};
        // Wakes when items_ is non-empty OR stop is requested
        bool ok = cv_.wait(lock, stoken, [&]{ return !items_.empty(); });
        if (!ok) return std::nullopt;   // stop_requested() became true
        T item = std::move(items_.front());
        items_.pop_front();
        return item;
    }
};

void worker(std::stop_token stoken, CancellableQueue<int>& q) {
    while (true) {
        auto item = q.pop(stoken);   // blocks; returns nullopt on cancellation
        if (!item) break;            // graceful shutdown
        // process *item ...
    }
}

// Usage
CancellableQueue<int> q;
std::jthread t{worker, std::ref(q)};  // std::jthread owns a stop_source
q.push(42);
t.request_stop();   // unblocks worker even if queue is empty

The stop-token registration and de-registration happen inside wait; the implementation is guaranteed race-free between a stop request and a new item arriving simultaneously.

Timed Waits: wait_for and wait_until

Both timed-wait overloads return true if the predicate became satisfied before the deadline, and false if the timeout expired first. The predicate-based overloads handle spurious wakeups automatically — the thread re-evaluates the predicate and goes back to sleep if needed, so the returned bool reliably distinguishes a real signal from a timeout.

cpp
// C++11
#include <condition_variable>
#include <mutex>
#include <chrono>
using namespace std::chrono_literals;

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

void requester() {
    std::unique_lock lock{mtx};

    // wait_for: relative duration — returns true if predicate fired in time
    bool ok = cv.wait_for(lock, 5s, [&]{ return result_ready; });
    if (!ok) {
        // Timed out; result_ready is still false, lock is re-held
        // Options: retry, return an error, or use a fallback
        throw std::runtime_error("timeout waiting for result");
    }
    // predicate is true here; process result ...
}

void responder() {
    // Absolute deadline: prefer steady_clock for relative timeouts;
    // use system_clock only when a wall-clock deadline is needed
    auto deadline = std::chrono::steady_clock::now() + 200ms;

    std::unique_lock lock{mtx};
    bool signaled = cv.wait_until(lock, deadline, [&]{ return result_ready; });
    if (!signaled) {
        // Handle missed deadline
    }
}

Never discard the return value of a timed wait. Code that falls through silently after a timeout and treats the condition as satisfied is a latent bug that surfaces only under load.

Implementing a Bounded Queue

A bounded queue blocks producers when the queue is full and blocks consumers when it is empty — two distinct waiting conditions that call for two separate condition variables. Using a single cv would force producers and consumers to wake each other needlessly; splitting them into not_full_ and not_empty_ keeps each side asleep unless the relevant condition changes.

cpp
// C++11
#include <condition_variable>
#include <mutex>
#include <deque>
#include <cstddef>

template<typename T>
class BoundedQueue {
    std::deque<T>           buf_;
    std::mutex              mtx_;
    std::condition_variable not_full_;
    std::condition_variable not_empty_;
    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 buf_.size() < cap_; });
        buf_.push_back(std::move(item));
        lock.unlock();
        not_empty_.notify_one();  // wake exactly one waiting consumer
    }

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

Unlocking before notifying (lock.unlock() then notify_one()) avoids the common pattern where the notified thread immediately re-blocks waiting for the mutex the notifier still holds.

Notification Patterns: notify_one vs notify_all

notify_one wakes a single waiting thread in O(1); notify_all wakes every waiting thread in O(n). Choosing the wrong one is a correctness issue, not just a performance concern: notify_one in a scenario where all waiters must react leaves threads permanently asleep, while notify_all in a single-producer scenario causes a thundering herd where N-1 threads wake, find the condition false, and re-block.

cpp
// C++11
std::mutex              mtx;
std::condition_variable cv;

// --- Producer-consumer: notify_one ---
// Exactly one item was added; exactly one consumer should take it.
void enqueue(Item item) {
    { std::lock_guard g{mtx}; queue.push(std::move(item)); }
    cv.notify_one();   // correct: any single consumer handles this item
}

// --- Broadcast/barrier: notify_all ---
// Configuration changed; every reader must re-load.
void broadcast_config_change() {
    { std::lock_guard g{mtx}; config_version++; }
    cv.notify_all();   // correct: all readers must react
}

// --- Shutdown: notify_all ---
// Every thread must observe the shutdown flag and exit.
bool shutdown_requested = false;
void request_shutdown() {
    { std::lock_guard g{mtx}; shutdown_requested = true; }
    cv.notify_all();   // correct: every waiting worker must wake up
}

// Classic bug: notify_one for a shutdown signal with N workers
// — only one worker wakes; the rest block forever.
void buggy_shutdown() {
    { std::lock_guard g{mtx}; shutdown_requested = true; }
    cv.notify_one();   // WRONG: N-1 threads never see shutdown
}

The decision rule is simple: use notify_one when producing a single consumable event; use notify_all when the state change is relevant to every waiter.

Condition Variable with shared_mutex (Read-Write Lock)

std::condition_variable only works with std::unique_lock<std::mutex>, but std::condition_variable_any works with any lockable — including std::shared_mutex — enabling condition waits that distinguish read and write phases. A writer acquires an exclusive lock, modifies data, calls notify_all, then releases. Readers acquire a shared lock, wait for the predicate, and proceed concurrently once the write completes.

cpp
// C++17
#include <condition_variable>
#include <shared_mutex>
#include <string>

std::shared_mutex           rw_mtx;
std::condition_variable_any cv;
std::string                 config;
bool                        config_ready = false;

void writer(std::string new_cfg) {
    {
        std::unique_lock lock{rw_mtx};   // exclusive write access
        config       = std::move(new_cfg);
        config_ready = true;
    }
    cv.notify_all();   // all readers may now proceed
}

void reader() {
    std::shared_lock lock{rw_mtx};   // shared read access
    cv.wait(lock, [&]{ return config_ready; });
    // Multiple readers hold shared_lock concurrently here
    auto local_copy = config;
    // use local_copy ...
}

This pattern is most useful when writes are rare and reads are concurrent and frequent. For simple flag-based synchronisation without concurrent reads, a plain std::mutex + std::condition_variable is sufficient and cheaper.

Semaphore vs Condition Variable (C++20)

std::counting_semaphore<N> expresses counting-based synchronisation more directly than a condition variable, but condition variables remain the right tool when the wake condition is an arbitrary predicate over shared state. Semaphore acquire() blocks until the internal counter is positive, then decrements it; release() increments the counter and unblocks a waiter. No mutex is needed.

cpp
// C++20
#include <semaphore>
#include <deque>
#include <mutex>

// --- Bounded queue with counting_semaphore ---
template<typename T>
class SemQueue {
    std::deque<T>                   buf_;
    std::mutex                      mtx_;        // protects buf_ only
    std::counting_semaphore<>       slots_;      // counts empty slots
    std::counting_semaphore<>       items_;      // counts filled slots

public:
    explicit SemQueue(std::ptrdiff_t cap)
        : slots_{cap}, items_{0} {}

    void push(T item) {
        slots_.acquire();                // block until a slot is free
        { std::lock_guard g{mtx_}; buf_.push_back(std::move(item)); }
        items_.release();                // signal that an item is available
    }

    T pop() {
        items_.acquire();                // block until an item exists
        T item;
        { std::lock_guard g{mtx_}; item = std::move(buf_.front()); buf_.pop_front(); }
        slots_.release();                // signal that a slot is free
        return item;
    }
};

The semaphore version is shorter and the synchronisation intent is visible in the counter semantics. The condition-variable version (see above) is more flexible: it can express predicates like !paused && !queue.empty() that combine multiple state variables, which would require multiple semaphores and careful ordering. Prefer semaphores for pure counting; prefer condition variables when the wake condition involves complex shared state.

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