C++ condition_variable | 조건 변수 완벽 가이드

C++ condition_variable | 조건 변수 완벽 가이드

이 글의 핵심

C++ condition_variable 완벽 가이드. 스레드 간 이벤트 통지를 위한 동기화 도구. wait·notify_one·notify_all로 생산자-소비자 패턴, 작업 큐, 배리어를 구현합니다. Spurious Wakeup 주의사항도 다룹니다.

들어가며

C++의 condition_variable스레드 간 이벤트 통지를 위한 동기화 도구입니다. 생산자-소비자 패턴, 작업 큐, 배리어 등을 구현할 때 사용합니다.

비유로 말씀드리면, condition_variable대기실에서 번호표를 들고 기다리는 것에 가깝습니다. 번호가 불리면 (notify) 깨어나서 (wait 종료) 작업을 시작합니다.

이 글을 읽으면

  • condition_variable의 개념과 사용법을 이해합니다
  • wait, notify_one, notify_all의 차이를 파악합니다
  • 생산자-소비자 패턴과 작업 큐를 구현합니다
  • Spurious Wakeup과 Lost Wakeup을 방지합니다

목차

  1. condition_variable 기초
  2. 실전 구현
  3. 고급 활용
  4. 성능 비교
  5. 실무 사례
  6. 트러블슈팅
  7. 마무리

condition_variable 기초

기본 개념

condition_variable조건이 충족될 때까지 스레드를 대기시키고, 조건이 충족되면 깨웁니다.

#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; });  // ready가 true일 때까지 대기
    
    std::cout << "작업 시작" << std::endl;
}

void mainThread() {
    {
        std::lock_guard<std::mutex> lock(mtx);
        ready = true;
    }
    cv.notify_one();  // 대기 중인 스레드 깨우기
}

int main() {
    std::thread t(worker);
    
    std::this_thread::sleep_for(std::chrono::seconds(1));
    mainThread();
    
    t.join();
    
    return 0;
}

실전 구현

1) 생산자-소비자 패턴

#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 << "생산: " << 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 << "소비: " << value << std::endl;
    }
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);
    
    t1.join();
    t2.join();
    
    return 0;
}

2) wait_for: 타임아웃

#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 << "조건 충족" << std::endl;
    } else {
        std::cout << "타임아웃" << std::endl;
    }
}

int main() {
    std::thread t(worker);
    
    // 2초 후 notify (타임아웃)
    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 << "스레드 " << id << " 깨어남" << 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: 하나만 깨움
    // cv.notify_one();
    
    // notify_all: 모두 깨움
    cv.notify_all();
    
    for (auto& t : threads) {
        t.join();
    }
    
    return 0;
}

고급 활용

1) 안전한 큐

#include <condition_variable>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>

template<typename T>
class SafeQueue {
private:
    std::queue<T> queue_;
    std::mutex mtx_;
    std::condition_variable cv_;
    bool done_ = false;
    
public:
    void push(T value) {
        {
            std::lock_guard<std::mutex> lock(mtx_);
            queue_.push(std::move(value));
        }
        cv_.notify_one();
    }
    
    bool pop(T& value) {
        std::unique_lock<std::mutex> lock(mtx_);
        cv_.wait(lock, [this]{ return !queue_.empty() || done_; });
        
        if (queue_.empty()) {
            return false;  // 종료
        }
        
        value = std::move(queue_.front());
        queue_.pop();
        return true;
    }
    
    void finish() {
        {
            std::lock_guard<std::mutex> lock(mtx_);
            done_ = true;
        }
        cv_.notify_all();
    }
};

int main() {
    SafeQueue<int> queue;
    
    std::thread producer([&queue]() {
        for (int i = 0; i < 10; ++i) {
            queue.push(i);
            std::cout << "생산: " << i << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
        }
        queue.finish();
    });
    
    std::thread consumer([&queue]() {
        int value;
        while (queue.pop(value)) {
            std::cout << "소비: " << value << std::endl;
        }
    });
    
    producer.join();
    consumer.join();
    
    return 0;
}

2) 작업 큐

#include <condition_variable>
#include <functional>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>

class TaskQueue {
private:
    std::queue<std::function<void()>> tasks_;
    std::mutex mtx_;
    std::condition_variable cv_;
    bool stop_ = false;
    
public:
    void addTask(std::function<void()> task) {
        {
            std::lock_guard<std::mutex> lock(mtx_);
            tasks_.push(std::move(task));
        }
        cv_.notify_one();
    }
    
    void worker() {
        while (true) {
            std::function<void()> task;
            
            {
                std::unique_lock<std::mutex> lock(mtx_);
                cv_.wait(lock, [this]{ return !tasks_.empty() || stop_; });
                
                if (stop_ && tasks_.empty()) {
                    return;
                }
                
                task = std::move(tasks_.front());
                tasks_.pop();
            }
            
            task();
        }
    }
    
    void shutdown() {
        {
            std::lock_guard<std::mutex> lock(mtx_);
            stop_ = true;
        }
        cv_.notify_all();
    }
};

int main() {
    TaskQueue queue;
    
    std::vector<std::thread> workers;
    for (int i = 0; i < 4; ++i) {
        workers.emplace_back(&TaskQueue::worker, &queue);
    }
    
    for (int i = 0; i < 10; ++i) {
        queue.addTask([i]() {
            std::cout << "작업 " << i << " 실행" << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
        });
    }
    
    std::this_thread::sleep_for(std::chrono::seconds(2));
    queue.shutdown();
    
    for (auto& t : workers) {
        t.join();
    }
    
    return 0;
}

3) 배리어

#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>

class Barrier {
private:
    std::mutex mtx_;
    std::condition_variable cv_;
    int count_;
    int waiting_ = 0;
    
public:
    Barrier(int count) : count_(count) {}
    
    void wait() {
        std::unique_lock<std::mutex> lock(mtx_);
        ++waiting_;
        
        if (waiting_ == count_) {
            waiting_ = 0;
            cv_.notify_all();
        } else {
            cv_.wait(lock, [this]{ return waiting_ == 0; });
        }
    }
};

void worker(int id, Barrier& barrier) {
    std::cout << "스레드 " << id << " 작업 1" << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(100 * id));
    
    barrier.wait();  // 모든 스레드 대기
    
    std::cout << "스레드 " << id << " 작업 2" << std::endl;
}

int main() {
    Barrier barrier(3);
    
    std::thread t1(worker, 1, std::ref(barrier));
    std::thread t2(worker, 2, std::ref(barrier));
    std::thread t3(worker, 3, std::ref(barrier));
    
    t1.join();
    t2.join();
    t3.join();
    
    return 0;
}

성능 비교

Busy Waiting vs condition_variable

#include <atomic>
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>

// Busy Waiting
std::atomic<bool> ready1{false};

void workerBusy() {
    while (!ready1) {
        // CPU 소모
    }
    std::cout << "Busy Waiting 완료" << std::endl;
}

// condition_variable
std::condition_variable cv;
std::mutex mtx;
bool ready2 = false;

void workerCV() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return ready2; });
    std::cout << "condition_variable 완료" << std::endl;
}

int main() {
    auto start1 = std::chrono::high_resolution_clock::now();
    std::thread t1(workerBusy);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    ready1 = true;
    t1.join();
    auto end1 = std::chrono::high_resolution_clock::now();
    auto time1 = std::chrono::duration_cast<std::chrono::milliseconds>(end1 - start1).count();
    
    auto start2 = std::chrono::high_resolution_clock::now();
    std::thread t2(workerCV);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    {
        std::lock_guard<std::mutex> lock(mtx);
        ready2 = true;
    }
    cv.notify_one();
    t2.join();
    auto end2 = std::chrono::high_resolution_clock::now();
    auto time2 = std::chrono::duration_cast<std::chrono::milliseconds>(end2 - start2).count();
    
    std::cout << "Busy Waiting: " << time1 << "ms (CPU 100%)" << std::endl;
    std::cout << "condition_variable: " << time2 << "ms (CPU 0%)" << std::endl;
    
    return 0;
}

결과:

방법CPU 사용률전력 소모
Busy Waiting100%높음
condition_variable0%낮음

결론: condition_variable이 효율적


실무 사례

사례 1: 스레드 풀

#include <condition_variable>
#include <functional>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>

class ThreadPool {
private:
    std::vector<std::thread> workers_;
    std::queue<std::function<void()>> tasks_;
    std::mutex mtx_;
    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(mtx_);
                        cv_.wait(lock, [this]{ return !tasks_.empty() || stop_; });
                        
                        if (stop_ && tasks_.empty()) {
                            return;
                        }
                        
                        task = std::move(tasks_.front());
                        tasks_.pop();
                    }
                    
                    task();
                }
            });
        }
    }
    
    ~ThreadPool() {
        {
            std::lock_guard<std::mutex> lock(mtx_);
            stop_ = true;
        }
        cv_.notify_all();
        
        for (auto& worker : workers_) {
            worker.join();
        }
    }
    
    void enqueue(std::function<void()> task) {
        {
            std::lock_guard<std::mutex> lock(mtx_);
            tasks_.push(std::move(task));
        }
        cv_.notify_one();
    }
};

int main() {
    ThreadPool pool(4);
    
    for (int i = 0; i < 10; ++i) {
        pool.enqueue([i]() {
            std::cout << "작업 " << i << " 실행" << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
        });
    }
    
    std::this_thread::sleep_for(std::chrono::seconds(2));
    
    return 0;
}

사례 2: 이벤트 시스템

#include <condition_variable>
#include <functional>
#include <iostream>
#include <mutex>
#include <queue>
#include <string>
#include <thread>

struct Event {
    std::string type;
    int data;
};

class EventSystem {
private:
    std::queue<Event> events_;
    std::mutex mtx_;
    std::condition_variable cv_;
    bool stop_ = false;
    
public:
    void emit(const Event& event) {
        {
            std::lock_guard<std::mutex> lock(mtx_);
            events_.push(event);
        }
        cv_.notify_one();
    }
    
    void processEvents() {
        while (true) {
            Event event;
            
            {
                std::unique_lock<std::mutex> lock(mtx_);
                cv_.wait(lock, [this]{ return !events_.empty() || stop_; });
                
                if (stop_ && events_.empty()) {
                    return;
                }
                
                event = events_.front();
                events_.pop();
            }
            
            std::cout << "이벤트: " << event.type << ", 데이터: " << event.data << std::endl;
        }
    }
    
    void shutdown() {
        {
            std::lock_guard<std::mutex> lock(mtx_);
            stop_ = true;
        }
        cv_.notify_all();
    }
};

int main() {
    EventSystem system;
    
    std::thread processor(&EventSystem::processEvents, &system);
    
    system.emit({"click", 100});
    system.emit({"keypress", 65});
    system.emit({"resize", 800});
    
    std::this_thread::sleep_for(std::chrono::seconds(1));
    system.shutdown();
    
    processor.join();
    
    return 0;
}

사례 3: 세마포어

#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>

class Semaphore {
private:
    std::mutex mtx_;
    std::condition_variable cv_;
    int count_;
    
public:
    Semaphore(int count) : count_(count) {}
    
    void acquire() {
        std::unique_lock<std::mutex> lock(mtx_);
        cv_.wait(lock, [this]{ return count_ > 0; });
        --count_;
    }
    
    void release() {
        {
            std::lock_guard<std::mutex> lock(mtx_);
            ++count_;
        }
        cv_.notify_one();
    }
};

void worker(int id, Semaphore& sem) {
    sem.acquire();
    
    std::cout << "스레드 " << id << " 작업 중" << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
    
    sem.release();
}

int main() {
    Semaphore sem(2);  // 최대 2개 스레드
    
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; ++i) {
        threads.emplace_back(worker, i, std::ref(sem));
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    return 0;
}

트러블슈팅

문제 1: Spurious Wakeup

증상: 조건 없이 깨어남

// ❌ 조건 없이 wait
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock);  // 가짜 깨어남 가능

if (ready) {  // 수동 체크
    // ...
}

// ✅ 조건과 함께 wait
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });  // 조건 자동 체크

문제 2: Lost Wakeup

증상: notify를 놓침

// ❌ notify 전에 wait
// 스레드 1
ready = true;
cv.notify_one();  // wait 전에 호출

// 스레드 2
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });  // 이미 notify 지나감

// ✅ 조건 변수와 플래그 함께 사용
// 스레드 1
{
    std::lock_guard<std::mutex> lock(mtx);
    ready = true;
}
cv.notify_one();

// 스레드 2
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });  // ready가 true면 즉시 통과

문제 3: 데드락

증상: 스레드가 영원히 대기

// ❌ lock 없이 notify
ready = true;  // 경쟁 조건
cv.notify_one();

// ✅ lock으로 보호
{
    std::lock_guard<std::mutex> lock(mtx);
    ready = true;
}
cv.notify_one();

문제 4: lock_guard vs unique_lock

증상: 컴파일 에러

// ❌ lock_guard는 wait에 사용 불가
std::lock_guard<std::mutex> lock(mtx);
cv.wait(lock);  // 에러: lock_guard는 unlock 불가

// ✅ unique_lock 사용
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock);  // OK: unique_lock은 unlock 가능

마무리

condition_variable스레드 간 이벤트 통지를 효율적으로 처리합니다.

핵심 요약

  1. condition_variable

    • 스레드 간 이벤트 통지
    • wait: 조건이 참일 때까지 대기
    • notify_one/notify_all: 대기 스레드 깨우기
  2. 사용법

    • unique_lock과 함께 사용
    • 조건 검사 필수 (Spurious Wakeup 방지)
    • lock으로 조건 변수 보호
  3. 패턴

    • 생산자-소비자
    • 작업 큐
    • 배리어
    • 세마포어
  4. 성능

    • Busy Waiting보다 효율적
    • CPU 사용률 낮음
    • 전력 소모 적음

선택 가이드

상황권장이유
생산자-소비자condition_variable효율적
작업 큐condition_variableCPU 절약
배리어std::barrier (C++20)표준
세마포어std::counting_semaphore (C++20)표준

코드 예제 치트시트

// 기본 사용
std::condition_variable cv;
std::mutex mtx;
bool ready = false;

// wait
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });

// notify
{
    std::lock_guard<std::mutex> lock(mtx);
    ready = true;
}
cv.notify_one();

// wait_for
if (cv.wait_for(lock, std::chrono::seconds(1), []{ return ready; })) {
    // 조건 충족
} else {
    // 타임아웃
}

다음 단계

  • 스레드 풀: C++ 스레드 풀
  • condition_variable 실무: C++ condition_variable 실무
  • 메모리 모델: C++ 메모리 모델

참고 자료

한 줄 정리: condition_variable은 스레드 간 이벤트 통지를 효율적으로 처리하며, Busy Waiting보다 CPU 사용률이 낮고 전력 소모가 적다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ 스레드 풀 | “Thread Pool” 구현 가이드
  • C++ condition_variable | “작업이 올 때만 깨워 주세요” 작업 큐
  • C++ 메모리 모델 | “동시성” 메모리 모델 가이드

관련 글

  • C++ condition_variable 실무 패턴 |
  • C++ 멀티스레드 크래시 |
  • C++ 메모리 모델 |
  • C++ std::thread 입문 | join 누락·디태치 남용 등 자주 하는 실수 3가지와 해결법
  • C++ 메모리 순서(Memory Ordering) 완벽 가이드 | relaxed·acquire/release