atomic & 비동기

MwG·2026년 3월 12일

C++

목록 보기
15/21

C++ 멀티스레딩 및 비동기

현대 C++의 동기화 도구들은 "안전성, 성능, 추상화"라는 세 가지 축을 중심으로 발전해 왔습니다.

1. 저수준의 제왕: std::atomic & memory_order

일반적인 mutex는 운영체제의 커널 리소스를 사용하므로 매우 무겁습니다. 스레드가 잠들고 깨어날 때 발생하는 Context Switch 비용을 줄이고, CPU 하드웨어가 제공하는 원자적 명령어를 직접 활용하기 위해 등장했습니다.

좋은 점극강의 성능: 락(Lock) 없이 데이터 오염을 막을 수 있습니다 (Lock-free).

미세 조정: memory_order를 통해 하드웨어 수준의 명령어 재배치를 제어할 수 있습니다.

추천 상황공유 카운터: 통계 수치, 방문자 수 계산.플래그(Flag): 스레드 종료 신호 전달.성능이 극도로 중요한 자료구조: Lock-free 큐, 스택 구현.

⚠️ 주의: 복잡한 로직을 아토믹으로만 짜는 것은 자살 행위와 같습니다. (버그 찾기가 불가능에 가까움)

2. 전통의 강자: std::mutex & std::condition_variable

여러 줄의 코드(임계 영역)를 하나의 원자적 단위로 묶어야 할 때, 그리고 스레드 간의 "신호(Signal)"를 주고받아야 할 때 필요합니다. 아토믹만으로는 "데이터가 준비될 때까지 잠들기"를 구현하기 어렵기 때문입니다.

좋은 점범용성: 가장 직관적이고 안전하게 데이터 레이스를 막을 수 있습니다.

자원 효율: condition_variable은 조건이 맞지 않으면 스레드를 잠재워 CPU 점유율을 0으로 만듭니다.

추천 상황복잡한 공유 자원 보호: 여러 변수를 한꺼번에 수정해야 할 때.생산자-소비자 패턴: 큐에 데이터가 들어올 때까지 기다려야 하는 상황.

3. 비동기의 추상화: future, promise, packaged_task

이들은 "함수의 결과값"을 어떻게 안전하게 전달할 것인가에 대한 고민에서 나왔습니다.

A. std::promise & std::future (수동 배달)

왜? 결과가 언제 나올지 모르는 상황에서 결과값을 담을 '우체통'이 필요해서.상황: 네트워크 응답 대기, 저수준 스레드 관리.

B. std::packaged_task (반자동 공장)

왜? 함수 실행과 결과 전달(promise)을 하나로 묶어 코드 중복을 줄이려고.상황: 스레드 풀(Thread Pool) 구현. 일감을 큐에 넣고 결과만 나중에 챙길 때.

C. std::async (완전 자동 배달)

왜? 스레드 생성, 관리, 결과 전달을 한 줄로 끝내고 싶어서.상황: 독립적인 계산 작업, 간단한 I/O 분리. 대부분의 일반적인 비동기 작업.

4. 상황별 도구 선택 가이드

1. 단순히 숫자 하나만 안전하게 올리고 싶다std::atomicMutex보다 훨씬 빠름여러

2. 스레드가 하나의 큐를 공유한다mutex + condition_variable가장 표준적이고 안전한 방식

3. 함수를 별도 스레드에서 돌리고 결과만 받고 싶다std::async가장 쉽고 관리가 필요 없음

4. 스레드 풀을 직접 구현하고 싶다std::packaged_task함수와 결과를 묶어 관리하기

5. 최적이벤트가 발생하면 모든 스레드를 동시에 깨우고 싶다 shared_future1:N 브로드캐스트가 가능함

5. 핵심 요약

복사보다 이동(std::move): 현대 C++ 비동기 객체들은 소유권의 유일성을 위해 복사를 금지합니다.

고수준 우선: 항상 가장 높은 추상화 수준(async)부터 고려하고, 성능이나 제어권이 부족할 때 저수준(atomic, promise)으로 내려가세요.

RAII 활용: 쌩 락(m.lock()) 대신 std::lock_guard나 std::unique_lock을 사용하여 예외 안전성을 확보하세요.

0개의 댓글