본문으로 건너뛰기 C++ condition_variable | Condition Variable Complete Guide

C++ condition_variable | Condition Variable Complete Guide

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

  1. condition_variable Basics
  2. Practical Implementation
  3. Advanced Usage
  4. Performance Comparison
  5. Practical Cases
  6. Troubleshooting
  7. 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

ApproachWakeup latencyCPU while idleComplexity
condition_variableOS-scheduler dependent (µs–ms)~0% (blocked)Medium (predicate + mutex discipline)
Busy-wait spin loopNear-instant100% of one coreLow, but wastes a whole core
std::atomic flag + short sleepBounded by sleep intervalLow but non-zeroLow, 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

  1. condition_variable: Tool for inter-thread event notification
  2. wait: Wait until condition is met
  3. notify_one: Wake one waiting thread
  4. notify_all: Wake all waiting threads
  5. 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)


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.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++, condition_variable, synchronization, threading, condition-variable.