1. Problem It Solves
A thread needs to wait for data, but a notification may arrive before it starts waiting. Record “data is ready” in state protected by a mutex, then use wait(lock, predicate) to check that state.
2. Prerequisites
Mutexes,
std::unique_lock, and data races: conflicting accesses across threads without the required synchronization.A producer that writes data and a consumer that reads it.
3. Core Idea
A condition variable does not store application events. A predicate tests a condition, such as ready == true, that determines whether the thread may continue.
wait(lock, pred) is equivalent to while (!pred()) wait(lock). When waiting is necessary, wait atomically unlocks the mutex and blocks; it locks the mutex again before returning. See the C++ wait rules.
4. Minimal Syntax
std::unique_lock lock(mutex);
condition.wait(lock, [&] { return ready; });
consume(payload);This is the consumer side; mutex, condition, ready, payload, and consume are already declared. The lock remains held while reading payload.
5. How It Works
The producer holds the same mutex, writes
payload, then setsready = true.It unlocks and calls
notify_one(). Notification while still holding the lock is also possible, but the consumer must wait to acquire it.If notification arrives early, the consumer sees
readyunder the lock and does not sleep. Recorded state avoids a lost wakeup: missing a notification and then waiting indefinitely despite available work.If
waitwakes without a corresponding notification, that is a spurious wakeup. The predicate is checked again; if false, waiting continues.Even after a real notification, another consumer may take the work before this thread reacquires the mutex. The condition must still be checked.
6. Common Mistakes
Using
if (!ready) wait(lock)and assuming every wake means data is available.Calling
notify_one()without recording state that can be checked again.Making
readyatomic but changing it outside the mutex without redesigning the waiting protocol. An atomic alone does not close the gap between checking the condition and starting to wait.Destroying the condition variable or shared data while another thread can still wait on or access them.
7. When to Use It
Use a condition variable when a thread should sleep until shared state allows progress: a queue has data, space becomes available, or shutdown is requested. A real queue often uses !queue.empty() || stopping as its predicate instead of a standalone notification flag.
8. Simple Example
The producer in the C++20 sample performs:
{
const std::lock_guard lock(mutex);
payload = 42;
ready = true;
}
condition.notify_one();The consumer uses the predicate above and then prints payload. It prints 42 regardless of which thread runs first. Both threads are joined before shared data is destroyed.
Complete sample code
Source file
dailycppinterview/219_condition-variable-predicates/main.cpp
// Real-World C++ Interviews Q219: Why must condition-variable waits use a predicate, and how do
// lost and spurious wakeups differ?
// Key: A condition variable carries no application state, so the protected predicate is the
// durable fact a waiter needs. Waiting atomically unlocks the associated mutex and blocks, then
// reacquires it before returning; the predicate overload rechecks in a loop because a wait may
// wake spuriously. A lost wakeup is a protocol bug in which notification occurs before a waiter
// is actually waiting and no recorded predicate preserves the event; checking and changing the
// predicate under the same mutex prevents that gap. Publish the state first, notify according
// to the protocol, and never treat notification alone as proof that the condition holds.
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
int main() {
std::mutex mutex;
std::condition_variable condition;
bool ready = false;
int payload = 0;
std::thread consumer([&] {
std::unique_lock lock(mutex);
condition.wait(lock, [&] { return ready; });
std::cout << payload << std::endl;
});
std::thread producer([&] {
{
const std::lock_guard lock(mutex);
payload = 42;
ready = true;
}
condition.notify_one();
});
producer.join();
consumer.join();
}
9. Key Takeaways
The mutex protects state; the predicate decides whether progress is allowed; notification prompts a waiter to check again. Receiving a notification alone does not prove the condition is true.
10. Self-Check Question
Full question: Why must condition-variable waits use a predicate, and how do lost and spurious wakeups differ?
If the producer sets ready = true and notifies before the consumer calls wait, why does the sample still work? What happens after a spurious wake while ready == false?