06장의 Thread Pool은 “작업을 어디서 실행할지"를 해결했지만, “그 작업의 결과를 어떻게 돌려받을지"는 답하지 않았다. 작업을 큐에 넣고 끝나면 그만인 fire-and-forget이 아니라, 계산 결과나 예외를 호출자가 안전하게 받아야 하는 경우가 훨씬 흔하다. 결과를 어디에 저장할 것인가? 아직 계산이 끝나지 않았다면 호출자는 어떻게 기다려야 하는가? 작업 중 예외가 발생하면 그 예외는 어느 스레드에서 다시 던져져야 하는가? 이 질문들에 락과 조건 변수를 직접 조합해 답하려면 매번 같은 보일러플레이트(완료 플래그, mutex, condition_variable, 결과/예외 저장소)를 작성해야 한다.
07장은 이 보일러플레이트를 표준 라이브러리가 어떻게 캡슐화했는지 다룬다. Promise와 Future는 “언젠가 완료될 작업의 결과"를 핸들로 나타내고, 그 결과(또는 예외)를 한 스레드에서 설정하고 다른 스레드에서 안전하게 대기·수신할 수 있게 한다. std::async와 std::packaged_task는 이 Promise/Future 메커니즘 위에 구축된 더 편리한 API다.
이 장을 읽기 전에
완전한 초보자? 이 장은 06장 「실행 관리 I: Thread Pool」에서 다룬 작업 큐(work queue)와 std::mutex/std::condition_variable의 기본 동작을 이미 안다고 가정합니다. 아직이라면 06장을 먼저 읽고 오세요. 또한 01장의 happens-before 개념(std::future::get()이 내부적으로 동기화 지점을 제공한다는 사실의 근거)을 가볍게 복습해 두면 좋습니다.
이 장의 깊이: 이 장은 중급~고급 수준입니다. std::promise/std::future의 기본 계약, std::async의 launch policy가 만드는 함정, 예외 전파의 정확한 의미, 그리고 packaged_task를 Thread Pool과 결합해 “Future를 반환하는 작업 큐"를 만드는 실전 패턴까지 다룹니다.
다루지 않는 것: std::shared_future를 이용한 다중 소비자 브로드캐스트, std::experimental::future의 .then() 체이닝(코루틴 기반 비동기 모델은 00장에서 명시한 대로 이 시리즈의 경계 밖입니다), 그리고 std::async의 구현별(libstdc++ vs MSVC) 스케줄링 차이의 세부 사항은 다루지 않습니다.
당신의 수준에 맞는 경로
| 수준 | 읽을 부분 | 핵심 목표 |
|---|---|---|
| 초보자 | “Promise와 Future” ~ “std::async” | 비동기 결과를 핸들로 받는다는 개념 이해 |
| 중급자 | “std::async” ~ “예외 처리와 전파 의미” | launch policy의 함정과 예외 전파 의미 이해 |
| 고급자 | “packaged_task” ~ “Thread Pool과 packaged_task 결합” | Future를 반환하는 작업 큐 구현 |
| 설계자 | “여러 Future 대기” ~ “안전성 검증” | 복잡한 비동기 시나리오와 검증 전략 |
Promise와 Future
Promise: 미래의 값을 설정하는 쪽 Future: 미래의 값을 받는 쪽
| |
특징:
- Promise는 이동만 가능 (복사 불가).
set_value()호출 전에get()을 호출하면 블로킹.- 예외도 전달 가능:
set_exception().
std::async
std::async는 Promise/Future를 직접 다루지 않고, 함수를 비동기로 실행하고 결과의 Future를 반환한다.
| |
Launch Policy의 세 가지 값:
std::launch::async: 새 스레드에서 즉시 실행을 강제한다. 호출 즉시 별도 스레드가 생성되어 작업을 시작한다.std::launch::deferred: 실행을 지연한다. 새 스레드를 만들지 않고, 반환된 future에서 처음.get()또는.wait()가 호출되는 시점에 호출자의 스레드에서 동기적으로 실행된다.std::launch::async | std::launch::deferred(인자를 생략한 기본값): 둘 중 어느 것을 쓸지는 구현이 결정한다. 표준은 어느 쪽을 선택해도 적합하다고만 규정한다.
Launch Policy의 함정
기본 정책(async | deferred)을 그대로 쓰면 다음과 같은 함정에 빠지기 쉽다.
| |
이 코드의 문제는 컴파일도 되고 결과도 맞지만, “백그라운드에서 미리 계산되고 있을 것"이라는 가정이 환경에 따라 거짓이 될 수 있다는 점이다. libstdc++/MSVC 모두 기본 정책에서 async를 선호하는 경향이 있지만, 표준이 보장하지 않으므로 다음 두 가지를 지켜야 한다.
- 진짜 병렬 실행이 필요하면
std::launch::async를 명시한다. std::async로 만든 future의 소멸자는, 그 future가std::launch::async로 시작된 작업에 연결되어 있고 아직 완료되지 않았다면, 작업이 끝날 때까지 블로킹한다. 즉std::async(std::launch::async, f);처럼 반환값을 버리면, 그 임시 future의 소멸자에서 암묵적으로join과 같은 대기가 발생한다 — 의도하지 않은 동기화 지점이 생기는 흔한 실수다.
| |
std::packaged_task
std::packaged_task는 호출 가능한 객체(함수, 람다 등)를 “패키징"해, 그 호출 결과를 std::future로 받을 수 있게 감싸는 래퍼다. std::async와 달리 언제, 어느 스레드에서 실행할지를 호출자가 직접 제어할 수 있어 Thread Pool과 결합하기 좋다.
| |
특징 정리:
std::promise처럼 이동만 가능, 복사 불가.get_future()는 단 한 번만 호출할 수 있다.- 패키징된 함수가 던진 예외는 자동으로
set_exception()을 거쳐 future에 저장된다 (직접 try/catch로 잡아set_exception을 호출할 필요가 없다).
예외 처리와 전파 의미
Promise/Future, std::async, packaged_task는 모두 예외도 값처럼 전달한다. 작업 도중 던져진 예외는 prom.set_exception(std::current_exception())을 통해 future의 공유 상태(shared state)에 저장되고, future::get()을 호출하는 시점에 호출자의 스레드에서 다시 던져진다(rethrow). 이 의미를 정확히 이해하는 것이 중요하다.
| |
핵심 의미는 세 가지다.
- 예외는 스레드 경계를 넘어 전달된다. worker 스레드에서 발생한 예외가 main 스레드의
catch블록에서 잡힌다 — 일반적인 C++ 예외는 스레드를 넘지 못하지만, future는 이를 안전하게 직렬화/재던지기로 우회한다. get()은 한 번만 결과(또는 예외)를 반환한다. 두 번째get()호출은std::future_error를 던진다.packaged_task와std::async도 동일한 메커니즘을 쓴다. 패키징된 함수 내부의 예외는 자동으로set_exception되므로, 다음처럼std::async가 호출한 함수가 던진 예외도get()에서 그대로 재발생한다.
| |
Thread Pool과 packaged_task 결합
06장의 Thread Pool은 void() 형태의 작업만 큐에 넣고 결과를 돌려주지 않았다. packaged_task로 작업을 감싸면, 임의의 반환형을 갖는 호출 가능 객체를 큐에 넣고 std::future로 결과를 받는 작업 큐를 만들 수 있다. 이것이 다음 08장의 Active Object에서 핵심 빌딩 블록이 된다.
| |
사용:
| |
enqueue가 던지는 작업이 예외를 일으켜도, packaged_task가 자동으로 set_exception하므로 fut.get()에서 그대로 재발생한다 — 위 “예외 처리와 전파 의미” 절의 의미가 Thread Pool 경계를 넘어서도 그대로 유지된다.
여러 Future 대기
여러 작업을 동시에 제출하고 모든 결과를 모으는 패턴은 매우 흔하다.
| |
이 패턴은 “모든 작업이 끝나야 다음으로 진행”(fork-join)에 적합하다. 작업 중 하나라도 예외를 던지면, 해당 f.get()에서 예외가 재발생하므로 나머지 future들은 여전히 백그라운드에서 실행 중일 수 있다는 점에 주의한다 — 필요하면 모든 future를 먼저 wait()한 뒤 예외를 처리하는 식으로 순서를 바꿔야 한다.
안전성 검증: ThreadSanitizer
Future/Promise 기반 코드에서 가장 흔한 실수는 packaged_task나 promise가 캡처한 this가 객체보다 먼저 소멸되는 경우다. 예를 들어 위 FutureThreadPool::enqueue에서 task 람다가 this(풀 자체)를 캡처하지 않도록 주의해야 풀이 소멸된 뒤에도 안전하다. 의심스러운 코드는 다음과 같이 TSAN으로 점검한다.
| |
TSAN은 set_value/set_exception과 get() 사이의 happens-before 관계가 깨졌을 때(예: 공유 상태에 직접 접근하는 잘못된 수동 구현) 데이터 레이스를 보고한다. 표준 std::future/std::promise 자체는 내부적으로 적절한 동기화를 제공하므로, 이 장의 예제처럼 표준 API만 사용하면 TSAN 경고가 발생하지 않는 것이 정상이다 — 경고가 뜬다면 future로 전달해야 할 데이터를 별도의 비동기 공유 변수로 우회 접근하고 있다는 신호다.
학습 성과 평가 기준
- Promise와 Future의 역할을 설명하고, 값을 설정/대기할 수 있는가?
-
std::async의 launch policy(asyncvsdeferredvs 기본값)와 그 차이가 만드는 함정을 설명할 수 있는가? -
future::get()이 예외를 재발생시키는 정확한 시점과 의미를 설명할 수 있는가? -
packaged_task로 함수를 패키징하고, Thread Pool과 결합해 임의 반환형의 결과를future로 받을 수 있는가? - 여러 future를 fork-join 방식으로 대기할 때의 예외 처리 순서를 이해하는가?
다음 장에서는
08장 **「비동기 객체 (Active Object)」**에서는 Promise/Future와 Thread Pool을 조합해 “요청-응답 큐를 가진 객체"를 만든다.
참고 및 출처
- Anthony Williams, 『C++ Concurrency in Action』, Chapter 4 — Future와 Promise 상세
- C++ Standards Committee,
<future>documentation
![Featured image of post [Concurrency Patterns] 07. 실행 관리 II: Future와 Promise](/post/multithreading-patterns/cpp-future-promise-async-packaged-task/wordcloud_hu_b54ca3586300f1be.webp)
![[Concurrency Patterns] 04. 데이터 흐름: Producer-Consumer](/post/multithreading-patterns/cpp-producer-consumer-bounded-buffer-backpressure/wordcloud_hu_5af444c86ef999cd.webp)
![[Concurrency Patterns] 05. 읽기 최적화와 지연 초기화](/post/multithreading-patterns/cpp-read-write-lock-dclp-call-once-lazy-init/wordcloud_hu_58028050dfa28943.webp)
![[Concurrency Patterns] 06. 실행 관리 I: Thread Pool](/post/multithreading-patterns/cpp-thread-pool-work-queue-work-stealing/wordcloud_hu_5141aa5781618afa.webp)
![[Concurrency Patterns] 07. 실행 관리 II: Future와 Promise](/post/multithreading-patterns/cpp-future-promise-async-packaged-task/wordcloud_hu_9444b48845ebd7f7.webp)
![[Concurrency Patterns] 08. 비동기 객체 (Active Object)](/post/multithreading-patterns/cpp-active-object-async-method-invocation/wordcloud_hu_254616021f48b2ef.webp)
![[Concurrency Patterns] 03. 대기와 조정](/post/multithreading-patterns/cpp-condition-variable-monitor-object-guarded-suspension/wordcloud_hu_3fa6659dfc8981c1.webp)
![[Concurrency Patterns] 02. 락 관용구](/post/multithreading-patterns/cpp-locking-idioms-scoped-locking-thread-safe-interface/wordcloud_hu_fa5a9d576f9499eb.webp)
![[Concurrency Patterns] 01. 동시성 기초와 C++ 메모리 모델](/post/multithreading-patterns/getting-started-cpp-concurrency-fundamentals-memory-model/wordcloud_hu_61c0c60d19b6fa49.webp)