Condition Variables
Condition variables allow threads to wait for specific conditions to become true, enabling efficient thread coordination beyond simple mutexes.
A condition variable can wake up spuriously โ wait() may return without any notify. So always
pass the predicate form, cv.wait(lock, []{ return ready; }), which loops until the condition holds;
the bare cv.wait(lock) is almost always a bug. The predicate also closes the lost wakeup race: if
a notify happens before you start waiting, the predicate is already true and you don't block. Note
the waiting thread must hold the mutex, and the thread that changes the condition should do so under
the same mutex before calling notify.
Basic Usageโ
#include <condition_variable>
#include <mutex>
#include <thread>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void waiter() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; }); // Wait until ready == true
// Continue when notified and ready == true
}
void notifier() {
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_one(); // Wake up one waiting thread
}
How Condition Variables Workโ
wait() Variantsโ
wait() with Predicateโ
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool data_ready = false;
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
// Equivalent to:
// while (!data_ready) {
// cv.wait(lock);
// }
cv.wait(lock, []{ return data_ready; });
processData();
}
Always use a predicate with wait() to handle spurious wakeupsโthe thread may wake even when not notified!
wait_for() - Timeoutโ
#include <chrono>
void consumerWithTimeout() {
std::unique_lock<std::mutex> lock(mtx);
if (cv.wait_for(lock, std::chrono::seconds(5), []{ return data_ready; })) {
// Condition met within 5 seconds
processData();
} else {
// Timeout
handleTimeout();
}
}
wait_until() - Absolute Timeโ
#include <chrono>
void consumerWithDeadline() {
std::unique_lock<std::mutex> lock(mtx);
auto deadline = std::chrono::system_clock::now() + std::chrono::seconds(10);
if (cv.wait_until(lock, deadline, []{ return data_ready; })) {
processData();
} else {
handleTimeout();
}
}
notify Variantsโ
notify_one()โ
Wakes up one waiting thread:
cv.notify_one(); // Wake one thread
notify_all()โ
Wakes up all waiting threads:
cv.notify_all(); // Wake all threads
Producer-Consumer Patternโ
#include <queue>
#include <mutex>
#include <condition_variable>
template<typename T>
class BlockingQueue {
std::queue<T> queue_;
std::mutex mutex_;
std::condition_variable cv_;
public:
void push(T value) {
{
std::lock_guard<std::mutex> lock(mutex_);
queue_.push(std::move(value));
}
cv_.notify_one(); // Wake a consumer
}
T pop() {
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this]{ return !queue_.empty(); });
T value = std::move(queue_.front());
queue_.pop();
return value;
}
};
// Usage
BlockingQueue<int> queue;
void producer() {
for (int i = 0; i < 10; ++i) {
queue.push(i);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void consumer() {
for (int i = 0; i < 10; ++i) {
int value = queue.pop(); // Blocks until data available
std::cout << "Consumed: " << value << '\n';
}
}
Practical Examplesโ
Example 1: Thread Pool Work Queueโ
#include <thread>
#include <vector>
#include <functional>
class ThreadPool {
std::vector<std::thread> workers_;
std::queue<std::function<void()>> tasks_;
std::mutex mutex_;
std::condition_variable cv_;
bool stop_ = false;
public:
ThreadPool(size_t numThreads) {
for (size_t i = 0; i < numThreads; ++i) {
workers_.emplace_back([this] {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this]{
return stop_ || !tasks_.empty();
});
if (stop_ && tasks_.empty()) return;
task = std::move(tasks_.front());
tasks_.pop();
}
task();
}
});
}
}
void enqueue(std::function<void()> task) {
{
std::lock_guard<std::mutex> lock(mutex_);
tasks_.push(std::move(task));
}
cv_.notify_one();
}
~ThreadPool() {
{
std::lock_guard<std::mutex> lock(mutex_);
stop_ = true;
}
cv_.notify_all();
for (auto& worker : workers_) {
worker.join();
}
}
};
Example 2: Barrier Synchronizationโ
#include <condition_variable>
class Barrier {
std::mutex mutex_;
std::condition_variable cv_;
size_t threshold_;
size_t count_ = 0;
size_t generation_ = 0;
public:
explicit Barrier(size_t count) : threshold_(count) {}
void wait() {
std::unique_lock<std::mutex> lock(mutex_);
size_t gen = generation_;
if (++count_ >= threshold_) {
++generation_;
count_ = 0;
cv_.notify_all();
} else {
cv_.wait(lock, [this, gen]{ return gen != generation_; });
}
}
};
// Usage
Barrier barrier(3);
void worker(int id) {
// Phase 1
doWork(id);
barrier.wait(); // All threads wait here
// Phase 2 - starts only when all reached barrier
doMoreWork(id);
}
Example 3: Event Notificationโ
#include <condition_variable>
class Event {
std::mutex mutex_;
std::condition_variable cv_;
bool signaled_ = false;
public:
void wait() {
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this]{ return signaled_; });
}
void signal() {
{
std::lock_guard<std::mutex> lock(mutex_);
signaled_ = true;
}
cv_.notify_all();
}
void reset() {
std::lock_guard<std::mutex> lock(mutex_);
signaled_ = false;
}
};
// Usage
Event startEvent;
void worker() {
startEvent.wait(); // Wait for start signal
doWork();
}
void coordinator() {
prepareWork();
startEvent.signal(); // Start all workers
}
Common Pitfallsโ
Pitfall 1: Lost Wakeupโ
// WRONG: notify before wait
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void thread1() {
ready = true;
cv.notify_one(); // Notifies, but no one waiting yet!
}
void thread2() {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; }); // May wait forever
}
// CORRECT: Check condition in predicate
void thread2_fixed() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; }); // Returns immediately if ready
}
Pitfall 2: Spurious Wakeupsโ
// WRONG: No predicate
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock); // May wake spuriously!
processData(); // Data might not be ready
}
// CORRECT: Always use predicate
void consumer_fixed() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return data_ready; }); // Handles spurious wakeups
processData();
}
Pitfall 3: Holding Lock During notifyโ
// LESS EFFICIENT
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
cv.notify_one(); // Lock held during notify
}
// MORE EFFICIENT
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
} // Lock released
cv.notify_one(); // Notify without holding lock
Condition Variable vs Busy-Waitโ
// BAD: Busy-wait (wastes CPU)
while (!ready) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
// GOOD: Condition variable (efficient)
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });
Best Practicesโ
- Always use a predicate with
wait() - Release locks before calling
notify() - Use
unique_lock(notlock_guard) with condition variables - Handle spurious wakeups with predicate
- Use
notify_all()when multiple threads wait
DON'T:
- Call
wait()without checking condition - Hold mutex while calling
notify()(inefficient) - Use condition variables without mutex
- Assume
wait()returns only when notified - Forget to notify waiting threads
When to Useโ
| Scenario | Use |
|---|---|
| Wait for data | Condition variable |
| Simple flag | Atomic + busy-wait |
| One-time event | std::promise/future |
| Periodic check | Timed wait |
Related Topicsโ
- Mutexes - Thread synchronization
- Futures and Promises - One-time values
- Threads - Thread basics
- Thread Pools - Worker coordination