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을 방지합니다
목차
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 Waiting | 100% | 높음 |
| condition_variable | 0% | 낮음 |
결론: 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은 스레드 간 이벤트 통지를 효율적으로 처리합니다.
핵심 요약
-
condition_variable
- 스레드 간 이벤트 통지
- wait: 조건이 참일 때까지 대기
- notify_one/notify_all: 대기 스레드 깨우기
-
사용법
- unique_lock과 함께 사용
- 조건 검사 필수 (Spurious Wakeup 방지)
- lock으로 조건 변수 보호
-
패턴
- 생산자-소비자
- 작업 큐
- 배리어
- 세마포어
-
성능
- Busy Waiting보다 효율적
- CPU 사용률 낮음
- 전력 소모 적음
선택 가이드
| 상황 | 권장 | 이유 |
|---|---|---|
| 생산자-소비자 | condition_variable | 효율적 |
| 작업 큐 | condition_variable | CPU 절약 |
| 배리어 | 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++ 메모리 모델
참고 자료
- “C++ Concurrency in Action” - Anthony Williams
- cppreference: https://en.cppreference.com/w/cpp/thread/condition_variable
- POSIX pthread 문서
한 줄 정리: 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