Skip to main content

Condition Variables

Condition variables allow threads to wait for specific conditions to become true, enabling efficient thread coordination beyond simple mutexes.

Always wait with a predicate

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();
}
info

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โ€‹

DO:
  • Always use a predicate with wait()
  • Release locks before calling notify()
  • Use unique_lock (not lock_guard) with condition variables
  • Handle spurious wakeups with predicate
  • Use notify_all() when multiple threads wait
danger

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โ€‹

ScenarioUse
Wait for dataCondition variable
Simple flagAtomic + busy-wait
One-time eventstd::promise/future
Periodic checkTimed wait