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++11A 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:
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
#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:
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
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:
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)
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:
// 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 falseThis 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:
// 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_allcauses 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:
// 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==trueThis 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.
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.
std::lock_guard g{mtx};
cv.wait(g, pred); // compile error: wait requires unique_lockwait calls lock.unlock() internally; lock_guard provides no unlock() method.
Ignoring the timed-wait return value.
cv.wait_for(lock, 100ms, pred); // discarded bool — was it a timeout?
// code below assumes condition is true — may not beSee Also
std::mutex— the required associated lockstd::unique_lock— the unlock-capable wrapperwait()requiresstd::semaphore— C++20; prefer over a CV when the count itself is the conditionstd::atomic— C++20atomic::wait()/notify_one()for single-variable conditions without a mutexstd::latch/std::barrier— C++20 one-shot and cyclic rendezvous points; cleaner than a CV for fixed-count coordinationstd::stop_token— C++20 cooperative cancellation mechanism