콘텐츠로 이동

C++20 Atomic Wait/Notify와 효율적인 동기화

C++20 이전에는 스레드 간 값 변경을 기다리려면 std::condition_variable이나 스핀루프를 사용해야 했습니다. C++20은 std::atomicwait(), notify_one(), notify_all()을 추가하여 OS 수준의 futex와 유사한 효율적인 대기를 제공합니다.


#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();
}

wait(old)는 다음과 같이 동작합니다.

  1. 현재 값을 로드
  2. 현재 값 == old이면 OS 수준에서 블로킹 (futex wait)
  3. notify_one() 또는 notify_all() 호출 시 깨어남
  4. 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();
}

// 스핀락: 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();

#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();

변경을 감지하는 일반적인 패턴입니다.

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));
}
}

waitnotify 호출 시 메모리 순서를 지정할 수 있습니다.

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();

// 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보다 코드 간결, 뮤텍스 불필요