C++20 Atomic Wait/Notify와 효율적인 동기화
C++20 이전에는 스레드 간 값 변경을 기다리려면 std::condition_variable이나 스핀루프를 사용해야 했습니다. C++20은 std::atomic에 wait(), notify_one(), notify_all()을 추가하여 OS 수준의 futex와 유사한 효율적인 대기를 제공합니다.
1. 기본 API
섹션 제목: “1. 기본 API”#include <atomic>#include <thread>#include <iostream>
std::atomic<int> value{0};
// wait(old): 현재 값이 old와 같으면 변경될 때까지 블로킹// notify_one(): 대기 중인 스레드 하나 깨움// notify_all(): 대기 중인 모든 스레드 깨움
void waiter() { value.wait(0); // value가 0이면 대기, 바뀌면 반환 std::cout << "값 변경됨: " << value.load() << "\n";}
void setter() { std::this_thread::sleep_for(std::chrono::milliseconds(100)); value.store(42); value.notify_one(); // 대기 중인 스레드 깨움}
int main() { std::thread t1(waiter); std::thread t2(setter); t1.join(); t2.join();}2. wait()의 동작 원리
섹션 제목: “2. wait()의 동작 원리”wait(old)는 다음과 같이 동작합니다.
- 현재 값을 로드
- 현재 값 ==
old이면 OS 수준에서 블로킹 (futex wait) notify_one()또는notify_all()호출 시 깨어남- spurious wakeup 가능 — 깨어난 후 반드시 조건 재확인 필요
std::atomic<bool> ready{false};
void consumer() { // spurious wakeup 방지: 루프로 재확인 while (!ready.load()) { ready.wait(false); // false면 대기 } std::cout << "준비 완료\n";}
void producer() { // 작업 수행 후 ready.store(true); ready.notify_all();}3. 스핀락과 비교
섹션 제목: “3. 스핀락과 비교”// 스핀락: CPU를 계속 점유 → 짧은 대기에는 빠르지만 긴 대기에는 비효율적std::atomic<bool> spinFlag{false};while (!spinFlag.load(std::memory_order_acquire)); // busy-wait
// atomic::wait: OS에 CPU 양보 → 긴 대기에 효율적spinFlag.wait(false); // 값이 false면 블로킹| 방법 | CPU 사용 | 응답성 | 적합한 대기 시간 |
|---|---|---|---|
| 스핀락 | 높음 (100%) | 매우 빠름 | 마이크로초 미만 |
| atomic::wait | 낮음 | 빠름 | 밀리초 이상 |
| condition_variable | 낮음 | 보통 | 밀리초 이상 |
4. std::atomic_flag를 이용한 이벤트
섹션 제목: “4. std::atomic_flag를 이용한 이벤트”std::atomic_flag는 C++20에서 wait/notify 지원이 추가되었습니다.
#include <atomic>
std::atomic_flag event = ATOMIC_FLAG_INIT;
void waitForEvent() { event.wait(false); // false면 대기 std::cout << "이벤트 수신\n";}
void signalEvent() { event.test_and_set(); // true로 설정 event.notify_one(); // 대기자 깨움}4.1 일회성 이벤트 (std::latch 대안)
섹션 제목: “4.1 일회성 이벤트 (std::latch 대안)”class OneShot { std::atomic_flag done_ = ATOMIC_FLAG_INIT;public: void signal() { done_.test_and_set(); done_.notify_all(); } void wait() { done_.wait(false); }};
OneShot init;
std::thread worker([&] { // 초기화 작업 init.signal();});
init.wait();std::cout << "초기화 완료\n";worker.join();5. 생산자-소비자 패턴
섹션 제목: “5. 생산자-소비자 패턴”#include <atomic>#include <queue>#include <mutex>#include <optional>
template<typename T>class AtomicQueue { std::queue<T> queue_; std::mutex mutex_; std::atomic<int> size_{0};
public: void push(T item) { { std::lock_guard lock(mutex_); queue_.push(std::move(item)); } size_.fetch_add(1); size_.notify_one(); // 소비자 깨움 }
T pop() { // 큐가 비어있으면 대기 int expected = 0; while (size_.load() == 0) { size_.wait(0); }
std::lock_guard lock(mutex_); T item = std::move(queue_.front()); queue_.pop(); size_.fetch_sub(1); return item; }};
AtomicQueue<int> queue;
std::thread producer([&] { for (int i = 0; i < 5; ++i) { queue.push(i); std::this_thread::sleep_for(std::chrono::milliseconds(10)); }});
std::thread consumer([&] { for (int i = 0; i < 5; ++i) { std::cout << "받음: " << queue.pop() << "\n"; }});
producer.join();consumer.join();6. 버전 카운터 패턴
섹션 제목: “6. 버전 카운터 패턴”변경을 감지하는 일반적인 패턴입니다.
struct SharedData { std::atomic<int> version{0}; std::string data; std::mutex mutex;};
SharedData shared;
void monitor() { int lastVersion = 0; while (true) { shared.version.wait(lastVersion); // 버전 변경 대기 lastVersion = shared.version.load();
std::lock_guard lock(shared.mutex); std::cout << "데이터 변경: " << shared.data << "\n"; }}
void updater() { for (int i = 0; i < 3; ++i) { { std::lock_guard lock(shared.mutex); shared.data = "업데이트 #" + std::to_string(i); } shared.version.fetch_add(1); shared.version.notify_all(); std::this_thread::sleep_for(std::chrono::milliseconds(500)); }}7. 메모리 순서와 wait
섹션 제목: “7. 메모리 순서와 wait”wait와 notify 호출 시 메모리 순서를 지정할 수 있습니다.
std::atomic<int> flag{0};
// 기본: seq_cst (가장 강한 순서 보장)flag.wait(0);
// 완화된 순서: 성능 최적화flag.wait(0, std::memory_order_acquire);flag.store(1, std::memory_order_release);flag.notify_one();8. condition_variable과 비교
섹션 제목: “8. condition_variable과 비교”// condition_variable 방식 (C++11~)std::mutex mtx;std::condition_variable cv;bool ready = false;
void old_wait() { std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, [] { return ready; });}
void old_signal() { { std::lock_guard lock(mtx); ready = true; } cv.notify_one();}
// atomic::wait 방식 (C++20~)std::atomic<bool> readyAtomic{false};
void new_wait() { readyAtomic.wait(false); // 훨씬 간결}
void new_signal() { readyAtomic.store(true); readyAtomic.notify_one();}atomic::wait는 뮤텍스 없이도 안전하게 동작하며 코드가 간결합니다. 단, condition_variable은 타임아웃 지정이 가능하다는 장점이 있습니다.
wait(old): 값이old이면 블로킹, 변경 시 반환 (spurious wakeup 가능)notify_one(): 대기 중인 스레드 하나 깨움notify_all(): 대기 중인 모든 스레드 깨움- OS futex 기반으로 스핀락보다 CPU 효율적
condition_variable보다 코드 간결, 뮤텍스 불필요