Condition Variables
Master efficient thread coordination using condition variables to signal between threads
Learn how to efficiently coordinate threads using condition variables, enabling threads to wait for specific conditions without busy-waiting.
A Simple Example
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex queueMutex;
std::condition_variable cv;
std::queue<int> dataQueue;
bool finished{false};
void producer(int id) {
for (int i{0}; i < 5; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
{
std::lock_guard<std::mutex> lock{queueMutex};
dataQueue.push(id * 10 + i);
std::cout << "Producer " << id << " added: " << (id * 10 + i) << "\n";
}
cv.notify_one(); // Wake up one waiting consumer
}
}
void consumer(int id) {
while (true) {
std::unique_lock<std::mutex> lock{queueMutex};
// Wait until queue has data or production is finished
cv.wait(lock, []{ return !dataQueue.empty() || finished; });
if (dataQueue.empty() && finished) {
break;
}
int value{dataQueue.front()};
dataQueue.pop();
lock.unlock();
std::cout << "Consumer " << id << " got: " << value << "\n";
}
}
int main() {
std::thread prod1{producer, 1};
std::thread prod2{producer, 2};
std::thread cons1{consumer, 1};
std::thread cons2{consumer, 2};
prod1.join();
prod2.join();
{
std::lock_guard<std::mutex> lock{queueMutex};
finished = true;
}
cv.notify_all();
cons1.join();
cons2.join();
return 0;
}
Breaking It Down
std::condition_variable basics
- What it does: Allows threads to wait for a condition to become true
- wait(): Releases lock and sleeps until notified, then reacquires lock
- notify_one(): Wakes up one waiting thread
- notify_all(): Wakes up all waiting threads
wait() with predicate
- Syntax: cv.wait(lock, []{ return condition; })
- Benefit: Handles spurious wakeups automatically
- How it works: Checks condition in a loop, sleeping if false
- Remember: Always use the predicate form to avoid spurious wakeups
std::unique_lock vs std::lock_guard
- unique_lock: Can unlock/relock manually, required for condition_variable
- lock_guard: Simple RAII, cannot be unlocked manually
- Why unique_lock: wait() needs to unlock the mutex while sleeping
- Remember: Use unique_lock with condition variables
Producer-consumer pattern
- Producer: Adds items to queue, notifies consumers
- Consumer: Waits for items, processes them
- Benefits: Decouples production and consumption, handles variable rates
- Remember: Always protect shared queue with mutex
Why This Matters
- Busy-waiting (checking a condition in a loop) wastes CPU cycles. Condition variables let threads sleep until they are actually needed.
- Producer-consumer patterns, task queues, and thread pools all rely on condition variables for efficient coordination.
- They are the foundation of most multithreaded synchronization patterns beyond simple mutex locking.
Critical Insight
Condition variables are like a waiting room with a receptionist. Instead of constantly asking "Is my appointment ready?" (busy-waiting), you sit down and the receptionist calls you when it is your turn.
wait() is you sitting down (and giving up your lock), notify_one() is the receptionist calling one person, and notify_all() is announcing that everyone can come in. The predicate checks if it is really your turn, handling cases where you were woken up by mistake.
Best Practices
Always use wait() with predicate: The form cv.wait(lock, []{ return condition; }) handles spurious wakeups automatically.
Use unique_lock with condition variables: std::unique_lock is required because wait() needs to unlock/relock.
Minimize lock scope: Unlock before doing expensive work after consuming data from the queue.
Use notify_one when possible: Only use notify_all if multiple threads need to wake up for the same event.
Check condition after waking: Even with predicates, always verify the condition is met before proceeding.
Common Mistakes
Forgetting the predicate: Without a predicate, spurious wakeups will cause bugs. Always use cv.wait(lock, predicate).
Using lock_guard: condition_variable requires unique_lock because wait() needs to unlock the mutex.
Notifying without lock: While legal, it can cause missed notifications. Hold the lock when changing the condition.
Not checking condition: Always verify the condition is actually met after wait() returns, even with predicates.
Debug Challenge
This condition variable wait is missing a critical component. Click the highlighted line to fix it:
Quick Quiz
- Why use condition variables instead of busy-waiting?
- What is a spurious wakeup?
- Why must condition_variable use unique_lock?
Practice Playground
Time to try out what you just learned! Play with the example code below, experiment by making changes and running the code to deepen your understanding.
Output:
Error:
Lesson Progress
- Fix This Code
- Quick Quiz
- Practice Playground - run once