C++ / CONCURRENCY AND PARALLELISM
Condition variables and waiting without spinning
Make a thread block on a mutex-protected predicate with std::condition_variable, notify it without losing wakeups, and add timeouts instead of polling.
What you will learn
- Use cv.wait(lock, pred) so spurious wakeups and stale state cannot fool a waiter
- Publish the state change under the mutex, then call notify_one or notify_all
- Pick notify_all when waiters test different predicates, notify_one when one can proceed
- Use wait_for with a predicate for bounded waits; the bool return is the predicate
Understanding Condition variables and waiting without spinning
Waiting for something another thread has to produce has two bad answers and one good one. Locking the mutex in a loop to re-check a flag burns a core and steals the very lock the producer needs in order to make progress, while sleeping for a fixed 10 ms adds latency you cannot tune away. std::condition_variable is the third option: wait(lock) releases the mutex and parks the calling thread as one indivisible step, then re-acquires the mutex before returning, so the thread uses no CPU while blocked and no notification can slip through the gap between checking and sleeping.
The name is misleading, because a condition variable holds no condition and no state at all. The condition is your own data behind the mutex; the cv is a parking lot with a doorbell, and a ring with nobody parked is simply discarded. That is why wait takes a predicate: cv.wait(lock, pred) is defined as while (!pred()) wait(lock);, so it tests first and never blocks on state that is already true, and it re-tests after every wakeup. It has to, because the standard permits spurious wakeups, and one notify_all can wake five threads when only one item exists.
The notifying side has a matching obligation: change the shared state while holding the mutex, then signal. Holding the mutex across the write is what forces every waiter into one of two safe positions, either it has not yet evaluated its predicate and will see the new state, or it is already parked and will receive the signal. The notify call itself needs no lock, and releasing first is usually better, since a thread woken while you still hold the mutex immediately blocks again on it. Whether you use notify_one or notify_all depends on how many parked threads could truthfully proceed: one queued item for one consumer is notify_one, a shutdown flag seen by five workers is notify_all.
<condition_variable>
<iostream>
<mutex>
<queue>
<thread>
std::mutex m;
std::condition_variable cv;
std::queue<int> jobs;
bool done = false;
void consumer() {
for (;;) {
std::unique_lock<std::mutex> lock(m);
// Sleeps here, and re-checks the predicate on every wakeup.
cv.wait(lock, [] { return !jobs.empty() || done; });
while (!jobs.empty()) {
std::cout << "consumed " << jobs.front() << '\n';
jobs.pop();
}
if (done) break; // queue drained and no more work is coming
}
std::cout << "consumer exiting\n";
}
int main() {
std::thread worker(consumer);
for (int i = 1; i <= 5; ++i) {
{
std::lock_guard<std::mutex> lock(m);
jobs.push(i);
}
cv.notify_one(); // state already published, mutex already released
}
{
std::lock_guard<std::mutex> lock(m);
done = true;
}
cv.notify_one();
worker.join();
}
A condition variable stores nothing, so correctness comes from re-checking mutex-protected state in a predicate; wait only decides when the thread sleeps.
Worked examples
wait_for returns the predicate, not the reason
Shows that the timed wait reports whether the condition holds, and that a satisfied predicate skips blocking entirely.
<chrono>
<condition_variable>
<iostream>
<mutex>
int main() {
std::mutex m;
std::condition_variable cv;
bool ready = false;
std::unique_lock<std::mutex> lock(m);
// Nobody will ever notify: this really sleeps 20 ms, then gives up.
bool ok = cv.wait_for(lock, std::chrono::milliseconds(20), [&] { return ready; });
std::cout << "first wait returned " << std::boolalpha << ok << '\n';
ready = true;
// Predicate is already true, so this returns at once without sleeping.
ok = cv.wait_for(lock, std::chrono::milliseconds(20), [&] { return ready; });
std::cout << "second wait returned " << ok << '\n';
}
Example explained
Line 1false from wait_for means the deadline passed and the predicate is still false, not that no notification arrived, so the return value is directly usable as a condition.
Line 2The second call returns true with no notify_one anywhere in the program, because wait_for evaluates the predicate before it considers blocking.
Line 3The mutex is held whenever the predicate runs and is re-locked before wait_for returns, which is why reading and writing ready needs nothing extra here.
A notification with no waiter is gone
Demonstrates that condition variables keep no pending signals, and that the predicate is what rescues a late waiter.
<condition_variable>
<iostream>
<mutex>
<thread>
int main() {
std::mutex m;
std::condition_variable cv;
int state = 0;
{
std::lock_guard<std::mutex> lock(m);
state = 1;
}
cv.notify_all(); // nothing is parked, so this signal is thrown away
std::cout << "notified before anyone waited\n";
std::thread late([&] {
std::unique_lock<std::mutex> lock(m);
// cv.wait(lock); here would block forever: the signal is long gone.
cv.wait(lock, [&] { return state == 1; });
std::cout << "waiter continued on state, not on the signal\n";
});
late.join();
}
Example explained
Line 1notify_all on an empty wait queue does nothing at all, since a condition variable has no counter and no memory of past signals.
Line 2cv.wait tests the predicate before parking, so the thread that starts waiting afterwards still sees state == 1 and returns immediately.
Line 3The commented-out bare cv.wait(lock) is the lost wakeup bug in one line: it waits for an event that already happened.
Turn-taking driven by shared state
Two threads alternate deterministically on one condition variable, each with its own predicate over the same guarded variable.
<condition_variable>
<iostream>
<mutex>
<thread>
std::mutex m;
std::condition_variable cv;
int turn = 0; // 0 = ping may print, 1 = pong may print
void play(const char* name, int mine, int next) {
for (int i = 0; i < 3; ++i) {
std::unique_lock<std::mutex> lock(m);
cv.wait(lock, [mine] { return turn == mine; });
std::cout << name << ' ' << i << '\n';
turn = next;
lock.unlock(); // release before ringing the doorbell
cv.notify_one();
}
}
int main() {
std::thread a(play, "ping", 0, 1);
std::thread b(play, "pong", 1, 0);
a.join();
b.join();
}
Example explained
Line 1Both threads park on the same cv and the same mutex, but each predicate names its own value of turn, so a wakeup only lets the thread whose turn it is continue.
Line 2turn is written while the mutex is held, which is why the other thread can never miss the change: it either has not checked yet, or it is parked and gets the notify.
Line 3lock.unlock() before notify_one means the woken thread can take the mutex straight away instead of waking up only to block on it.
Line 4The order of the six lines is fixed by turn rather than by timing, so the program prints the same output on every run.
Important notes
Every thread waiting on one condition_variable must pass the same mutex; using two different mutexes with a single cv is undefined behaviour. To wait while holding something other than unique_lock<mutex>, such as a shared_lock, use std::condition_variable_any, which buys that flexibility with an extra internal mutex.
The C++20 overload that takes a std::stop_token, so a jthread stop request wakes the waiter for you, exists on std::condition_variable_any only, not on std::condition_variable.
Common mistakes
Writing if (jobs.empty()) cv.wait(lock); instead of passing a predicate: one spurious wakeup, or a notify_all that woke three threads for a single item, and the thread pops from an empty queue, which is undefined behaviour rather than an error you can catch.
Pushing the item or setting the flag without holding the mutex and then notifying: the waiter can be past its predicate check but not yet parked, the notify reaches nobody, and the program hangs with the data sitting there ready.
Letting main return while a worker is still inside wait, because there is no shutdown flag in the predicate and no join: the condition variable and mutex are destroyed under a live waiter, which is undefined behaviour and usually shows up as a crash or hang at exit.
Try it yourself
Change, predict, then run
Extend the producer/consumer program so the queue never holds more than two items: add a second condition variable that the consumer notifies after each pop, and have the producer wait on it with the predicate jobs.size() < 2. The five values should still print in order from 1 to 5.
Open the C++ workspaceCheck your understanding
A consumer holds the mutex, sees the queue empty, and calls cv.wait(lock, pred). At that same instant the producer pushes an item and calls notify_one. Why can the consumer not miss that item forever?
- Condition variables buffer notifications, so the signal is delivered to the next thread that starts waiting.
- notify_one blocks until some waiter has acknowledged the signal.
- The producer needs the same mutex to push, and wait releases that mutex only as part of atomically parking the thread, so the push and notify happen either entirely before the predicate check or entirely after the thread is parked.
- The runtime re-evaluates the waiter's predicate on a timer, so it notices the new item shortly afterwards.
Show answer
Unlocking and parking is one indivisible step, and the producer cannot touch the queue without the mutex, so there is no window in which the state change is invisible to a thread that is not yet asleep. Option 0 is tempting because buffering would also prevent lost wakeups, but a condition variable stores nothing: a notify with no waiter is discarded, which is exactly why the predicate over your own state, not the signal, is the source of truth.