C++ condition_variable | Condition Variable Complete Guide
이 글의 핵심
C++ condition_variable synchronization tool for inter-thread event notification. Implement producer-consumer pattern, work queue, and barrier with wait, notify_one, notify_all, wait_for.
Introduction
C++‘s condition_variable is a synchronization tool for inter-thread event notification. Used to implement producer-consumer pattern, work queue, barrier, etc. To use an analogy, condition_variable is like waiting with a number ticket in a waiting room. When your number is called (notify), you wake up (wait ends) and start working.
After Reading This
- Understand concept and usage of condition_variable
- Grasp differences between wait, notify_one, notify_all
- Implement producer-consumer pattern and work queue
- Prevent Spurious Wakeup and Lost Wakeup
Table of Contents
- condition_variable Basics
- Practical Implementation
- Advanced Usage
- Performance Comparison
- Practical Cases
- Troubleshooting
- Conclusion
condition_variable Basics
Basic Concept
condition_variable waits for a thread until condition is met, and wakes it when condition is met.
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
std::condition_variable cv;
std::mutex mtx;
bool ready = false;
void worker() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; }); // Wait until ready is true
std::cout << "Start work" << std::endl;
}
void mainThread() {
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_one(); // Wake waiting thread
}
int main() {
std::thread t(worker);
std::this_thread::sleep_for(std::chrono::seconds(1));
mainThread();
t.join();
return 0;
}
Practical Implementation
1) Producer-Consumer Pattern
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>
std::queue<int> q;
std::mutex mtx;
std::condition_variable cv;
void producer() {
for (int i = 0; i < 10; ++i) {
{
std::lock_guard<std::mutex> lock(mtx);
q.push(i);
std::cout << "Produce: " << i << std::endl;
}
cv.notify_one();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void consumer() {
for (int i = 0; i < 10; ++i) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return !q.empty(); });
int value = q.front();
q.pop();
lock.unlock();
std::cout << "Consume: " << value << std::endl;
}
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
2) wait_for: Timeout
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
std::condition_variable cv;
std::mutex mtx;
bool ready = false;
void worker() {
std::unique_lock<std::mutex> lock(mtx);
if (cv.wait_for(lock, std::chrono::seconds(1), []{ return ready; })) {
std::cout << "Condition met" << std::endl;
} else {
std::cout << "Timeout" << std::endl;
}
}
int main() {
std::thread t(worker);
// Notify after 2 seconds (timeout)
std::this_thread::sleep_for(std::chrono::seconds(2));
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_one();
t.join();
return 0;
}
3) notify_one vs notify_all
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
std::condition_variable cv;
std::mutex mtx;
bool ready = false;
void worker(int id) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });
std::cout << "Thread " << id << " woke up" << std::endl;
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.emplace_back(worker, i);
}
std::this_thread::sleep_for(std::chrono::seconds(1));
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
// notify_one: Wake one only
// cv.notify_one();
// notify_all: Wake all
cv.notify_all();
for (auto& t : threads) {
t.join();
}
return 0;
}
Advanced Usage
wait_until: Absolute deadline instead of a duration
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
if (cv.wait_until(lock, deadline, []{ return ready; })) {
// condition met before the deadline
}
Prefer wait_until over wait_for when you’re coordinating a deadline shared across multiple waiters — computing it once up front avoids each thread’s wait_for restarting its own countdown after being woken by an unrelated notification.
condition_variable_any: waiting with something other than std::mutex
std::condition_variable only works with std::unique_lock<std::mutex>. If you need to wait while holding a std::shared_lock, a custom lockable type, or want to combine a wait with a std::stop_token (C++20), use std::condition_variable_any instead — it’s more general but has more overhead per wait, so don’t reach for it by default.
std::condition_variable_any cv;
std::shared_mutex smtx;
// works with shared_lock, unique_lock, or any BasicLockable type
Cooperative cancellation with jthread + stop_token (C++20)
std::condition_variable_any cv;
std::mutex mtx;
void worker(std::stop_token st) {
std::unique_lock lock(mtx);
cv.wait(lock, st, [] { return some_condition(); }); // wakes on stop_request() too
}
std::jthread t(worker);
t.request_stop(); // wakes any waiting condition_variable_any immediately
This is the modern, race-free way to interrupt a waiting thread — no more polling a manual std::atomic<bool> stop_flag in a loop.
Performance Comparison
| Approach | Wakeup latency | CPU while idle | Complexity |
|---|---|---|---|
condition_variable | OS-scheduler dependent (µs–ms) | ~0% (blocked) | Medium (predicate + mutex discipline) |
| Busy-wait spin loop | Near-instant | 100% of one core | Low, but wastes a whole core |
std::atomic flag + short sleep | Bounded by sleep interval | Low but non-zero | Low, imprecise |
std::binary_semaphore (C++20) | Similar to condition_variable | ~0% | Lower than CV for simple signal-only cases |
condition_variable is the right default for “wait for an arbitrary predicate over shared state.” If you only need a simple one-shot or counting signal with no associated shared state to check, std::counting_semaphore/std::binary_semaphore (C++20) is lighter weight and harder to misuse — there’s no separate mutex to forget to hold while checking the condition.
Practical Cases
Case 1: Thread pool task queue
A fixed set of worker threads cv.wait() on a shared task queue’s “not empty” predicate; the pool’s submit() pushes a task and calls notify_one() — exactly the producer-consumer pattern above, generalized from one producer/consumer pair to N workers.
Case 2: Bounded buffer with backpressure
When the queue also has a maximum size, producers need a second condition variable (or the same one with a different predicate) to wait on “not full” — so slow consumers naturally block fast producers instead of the queue growing without bound.
Case 3: Graceful shutdown signaling
A shutdown flag guarded by the same mutex, checked in the wait predicate (cv.wait(lock, [] { return !queue.empty() || shutdown; })) lets a single notify_all() at shutdown wake every blocked worker so they can observe the flag and exit cleanly, instead of leaving them parked forever.
Troubleshooting
Lost wakeup: notify happens before wait
Symptom: a thread calls wait() and blocks forever even though the condition became true.
Cause: the notifying thread set the state and called notify_one() before the waiting thread reached wait() — without a predicate, that notification is simply gone by the time anyone is listening.
Fix: always pass a predicate to wait() (as in every example above). The predicate is checked under the lock before blocking, so a notification that “arrived early” is still caught by the initial check.
Spurious wakeups
Symptom: the waiting thread wakes up even though nothing notified it.
Cause: this is allowed by the standard — wait() without a predicate can return for no logical reason, and OS-level implementations do occasionally do this.
Fix: same as above — the predicate overload (cv.wait(lock, predicate)) loops internally until the predicate is actually true, so spurious wakeups are transparently absorbed.
Thundering herd from notify_all
Symptom: waking N threads with notify_all() causes a burst of contention as they all immediately compete for the same mutex.
Cause: every woken thread re-checks its predicate under the lock, one at a time, even though only one (or a few) may actually have work to do.
Fix: use notify_one() when only one waiter can make progress (the producer-consumer case above), and reserve notify_all() for cases like shutdown where every waiter genuinely needs to react.
Summary
Key Points
- condition_variable: Tool for inter-thread event notification
- wait: Wait until condition is met
- notify_one: Wake one waiting thread
- notify_all: Wake all waiting threads
- unique_lock: Required for wait (can unlock/lock)
When to Use
✅ Use condition_variable when:
- Producer-consumer pattern
- Work queue
- Event notification
- Thread synchronization ❌ Don’t use when:
- Simple flag checking (use atomic)
- No waiting needed (use mutex only)
- Performance critical tight loops
Best Practices
- ✅ Always check condition in wait predicate
- ✅ Use unique_lock for wait
- ✅ Minimize critical section
- ✅ Handle Spurious Wakeup
- ❌ Don’t forget to notify
- ❌ Don’t hold lock while notifying (can, but less efficient)
Related Articles
- C++ Multithreading Basics
- C++ Thread Pool
- C++ Condition Variable Deep Dive Master thread synchronization with condition_variable! 🚀
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. C++ condition_variable complete guide. Synchronization tool for inter-thread event notification.
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- C++ 멀티스레딩 | ‘thread/mutex’ 기초 가이드
- C++ condition_variable 실무 패턴 | ‘작업이 올 때만 깨워 주세요’ 작업 큐
- C++ 스레드 풀 | ‘Thread Pool’ 구현 가이드
Keywords Covered in This Article (Related Search Terms)
This article covers C++, condition_variable, synchronization, threading, condition-variable.