C++ shared_future | 여러 스레드에서 future 결과 공유
이 글의 핵심
여러 워커 스레드가 한 번 계산된 설정값이나 캐시 결과를 기다려야 할 때 std::future는 get()을 한 번만 허용해 두 번째 호출이 실패합니다. 이 글은 shared_future가 복사 가능하고 get()을 여러 번 호출할 수 있는 이유, 스레드마다 자기 복사본을 가져야 안전하다는 점, 예외가 모든 대기자에게 다시 던져지는 동작을 예제로 보여줍니다.
shared_future란?
std::future는 결과를 한 번만 get() 할 수 있으며, 이동만 가능합니다. std::shared_future는 복사 가능하며, 여러 스레드가 각자 복사본을 가지고 동일한 비동기 결과를 get() 할 수 있습니다. promise와 future, async로 만든 결과를 여러 스레드가 나눠 쓸 때 유용하며, 스레드 기초를 알고 있으면 이해하기 쉽습니다.
future와의 차이
| 특성 | std::future | std::shared_future |
|---|---|---|
| 복사 | 불가 (이동만 가능) | 가능 |
| get() 횟수 | 한 번만 (이후 invalid) | 제한 없음 (valid 유지) |
| get() 반환 | T (값을 이동해 꺼냄) | const T& (공유 상태 참조) |
| 스레드 공유 | 불가 | 가능 (복사본 전달) |
| 사용 시나리오 | 단일 스레드 결과 수신 | 여러 스레드 결과 공유 |
// future: 이동만 가능
std::future<int> f1 = std::async([]{ return 42; });
// std::future<int> f2 = f1; // 에러! 복사 불가
std::future<int> f2 = std::move(f1); // OK: 이동
int result = f2.get(); // OK
// int result2 = f2.get(); // 에러! 두 번째 get() 불가
// shared_future: 복사 가능
std::shared_future<int> sf1 = std::async([]{ return 42; }).share();
std::shared_future<int> sf2 = sf1; // OK: 복사
int r1 = sf1.get(); // OK
int r2 = sf2.get(); // OK: 다른 복사본에서 get()
두 타입의 차이는 get()의 반환 타입에 그대로 드러납니다. std::future<T>::get()은 결과를 이동해서 꺼내 가는 연산이라 T를 값으로 돌려주고, 호출 후 valid()가 false가 되어 다시 부르면 std::future_error(no_state)를 던지거나 구현에 따라 미정의 동작이 됩니다. 반면 shared_future<T>::get()은 공유 상태에 저장된 값을 const T&로 빌려주기만 하므로 몇 번을 불러도 같은 값을 돌려줍니다. 이 때문에 shared_future<std::unique_ptr<X>>처럼 이동 전용 타입을 담으면 get()으로 값을 가져올 수 없고(const 참조라 이동 불가) 참조로만 써야 한다는 제약이 생깁니다. 반환된 참조는 공유 상태를 가리키므로, 그 상태를 참조하는 shared_future가 하나라도 살아 있는 동안만 유효하다는 점도 기억해 두세요.
기본 사용
#include <future>
#include <iostream>
#include <thread>
int main() {
std::promise<int> p;
std::shared_future<int> f = p.get_future().share();
std::thread t1([f]() { std::cout << "t1: " << f.get() << '\n'; });
std::thread t2([f]() { std::cout << "t2: " << f.get() << '\n'; });
p.set_value(42);
t1.join();
t2.join();
return 0;
}
동작 원리: share()를 호출하면 기존 future는 invalid 상태가 되고, 반환된 shared_future를 복사해 각 스레드에 전달합니다. 람다에서 값으로 캡처 [f] 하면 스레드마다 복사본이 생기므로 각자 get() 한 번씩 호출할 수 있습니다.
실무 팁:
- 값 캡처 사용:
[f]로 값 캡처하면 각 스레드가 독립적인 복사본을 가짐 - 참조 캡처 주의:
[&f]로 참조 캡처하면 여러 스레드가 같은shared_future객체에 접근하게 됨. 표준은 “각 스레드가 자기 복사본을 통해 접근할 때” 공유 상태 접근이 안전하다고 보장하므로, 같은 객체를 여러 스레드가 만지는 구조는 피하는 것이 원칙이고, 특히 한 스레드가 그 객체에 대입(f = ...)하는 동안 다른 스레드가get()하면 명백한 데이터 레이스
// ✅ 올바른 사용: 값 캡처
std::shared_future<int> sf = ...;
std::thread t1([sf]() { int r = sf.get(); }); // 복사본
std::thread t2([sf]() { int r = sf.get(); }); // 복사본
// ❌ 잘못된 사용: 참조 캡처
std::thread t3([&sf]() { int r = sf.get(); }); // 같은 객체
std::thread t4([&sf]() { int r = sf.get(); }); // 피해야 할 패턴 (표준 보장 밖)
참조 캡처에는 스레드 안전성 말고도 수명 문제가 있습니다. sf가 지역 변수이고 스레드가 join 전에 함수가 반환되거나 sf에 다른 값이 대입되면, 참조로 캡처한 스레드는 파괴되었거나 바뀐 객체를 봅니다. 값 캡처는 복사본이 스레드 수명만큼 살아 있으므로 두 문제를 동시에 없애 줍니다. 복사 비용은 공유 상태에 대한 참조 카운트를 하나 올리는 정도라 거의 무시할 수 있습니다.
예외 전파
promise에서 set_exception으로 예외를 넣으면, shared_future를 get()하는 모든 스레드에서 동일한 예외가 전파됩니다. 여러 스레드가 같은 실패 결과를 처리할 때 일관성이 있습니다.
#include <future>
#include <iostream>
#include <thread>
#include <exception>
std::promise<int> p;
auto sf = p.get_future().share();
std::thread t1([sf]() {
try {
int result = sf.get();
std::cout << "t1: " << result << '\n';
}
catch (const std::exception& e) {
std::cout << "t1 caught: " << e.what() << '\n';
}
});
std::thread t2([sf]() {
try {
int result = sf.get();
std::cout << "t2: " << result << '\n';
}
catch (const std::exception& e) {
std::cout << "t2 caught: " << e.what() << '\n';
}
});
// 예외 설정
p.set_exception(std::make_exception_ptr(std::runtime_error("failed")));
t1.join(); // "t1 caught: failed"
t2.join(); // "t2 caught: failed"
동작 원리: shared_future는 내부적으로 공유 상태(shared state) 를 참조 카운팅으로 관리합니다. 예외가 설정되면, 모든 복사본에서 get() 호출 시 동일한 예외가 다시 던져집니다.
실무 활용: 데이터베이스 연결 실패 시, 모든 워커 스레드가 동일한 예외를 받아 일관되게 처리할 수 있습니다.
여기서 “동일한 예외”는 문자 그대로 같은 예외 객체입니다. 공유 상태에는 std::exception_ptr 하나가 저장되고, 각 스레드의 get()은 이를 std::rethrow_exception으로 다시 던지므로, 여러 스레드가 동시에 같은 예외 객체를 catch하게 됩니다. 그래서 catch (std::exception& e)로 받아 e의 멤버를 수정하는 코드는 스레드 간 데이터 레이스가 되며, 예외는 const 참조로 받아 읽기만 하는 것이 안전합니다.
아래 DB 예제에서 워커가 auto conn = conn_future.get();으로 받는 부분은 DatabaseConnection을 복사합니다. 연결 객체가 복사 불가능한 타입이면 컴파일되지 않고, 복사 가능하더라도 네 워커가 하나의 소켓을 동시에 쓰게 되어 위험합니다. 실제로는 const auto& conn으로 받아 읽기 전용으로 쓰거나, 연결 풀처럼 스레드마다 별도 연결을 주는 구조가 맞습니다. connector가 [&]로 conn_promise를 참조 캡처하는 것은 join 전에 promise가 파괴되지 않으므로 안전합니다.
std::promise<DatabaseConnection> conn_promise;
auto conn_future = conn_promise.get_future().share();
std::thread connector([&]() {
try {
auto conn = connect_to_database();
conn_promise.set_value(std::move(conn));
} catch (...) {
conn_promise.set_exception(std::current_exception());
}
});
// 여러 워커가 연결 대기
std::vector<std::thread> workers;
for (int i = 0; i < 4; ++i) {
workers.emplace_back([conn_future]() {
try {
auto conn = conn_future.get();
// ... DB 작업 ...
} catch (const std::exception& e) {
std::cerr << "Worker failed: " << e.what() << '\n';
}
});
}
connector.join();
for (auto& w : workers) w.join();
스레드 안전성
shared_future는 스레드 안전한가?:
- 복사본 간: 안전 (각 복사본은 독립적)
- 같은 객체: 표준 보장 밖 (여러 스레드가 같은 객체를 공유하지 말 것)
std::shared_future<int> sf = ...;
// ✅ 안전: 각 스레드가 복사본 사용
std::thread t1([sf]() { int r = sf.get(); }); // sf의 복사본
std::thread t2([sf]() { int r = sf.get(); }); // sf의 복사본
// ❌ 불안전: 같은 객체를 참조로 공유
std::thread t3([&sf]() { int r = sf.get(); }); // 같은 sf
std::thread t4([&sf]() { int r = sf.get(); }); // 같은 객체 공유: 피할 것
내부 동작: shared_future는 내부적으로 공유 상태(shared state) 를 가리키는 포인터를 가지고 있습니다. 복사하면 포인터가 복사되고 참조 카운트가 증가합니다. get()은 결과가 준비될 때까지 기다린 뒤 공유 상태의 값을 참조로 돌려줄 뿐 상태를 바꾸지 않으며, 공유 상태에 대한 이런 접근은 각 스레드가 자기 복사본을 통해 할 때 안전하다고 표준이 보장합니다. 같은 shared_future 객체를 여러 스레드가 참조로 공유하는 경우는 이 보장의 범위 밖에 있어, 그 객체에 대입이나 이동이 섞이는 순간 데이터 레이스가 됩니다. “스레드마다 복사본”을 규칙으로 두면 이런 경계를 따질 필요가 없어집니다.
권장 패턴:
// ✅ 패턴 1: 값 캡처로 복사본 전달
std::shared_future<int> sf = ...;
std::thread t1([sf]() { /* ... */ });
std::thread t2([sf]() { /* ... */ });
// ✅ 패턴 2: 명시적 복사 후 참조 전달
std::shared_future<int> sf1 = sf;
std::shared_future<int> sf2 = sf;
std::thread t1([&sf1]() { /* ... */ });
std::thread t2([&sf2]() { /* ... */ });
// ❌ 패턴 3: 참조 캡처 (같은 객체 공유, 피해야 함)
std::thread t1([&sf]() { /* ... */ });
std::thread t2([&sf]() { /* ... */ });
실전 예시: 여러 워커가 한 번의 설정값 사용
설정을 비동기로 로드한 뒤, 여러 스레드가 그 결과를 읽는 패턴입니다.
#include <future>
#include <thread>
#include <vector>
#include <iostream>
struct Config {
std::string db_url;
int max_connections;
};
Config load_config_from_disk() {
// ... 파일에서 설정 로드 ...
return {"localhost:5432", 10};
}
void do_work(const Config& cfg) {
std::cout << "Working with DB: " << cfg.db_url << '\n';
}
int main() {
std::promise<Config> config_promise;
std::shared_future<Config> config_future = config_promise.get_future().share();
// 설정 로더 스레드
std::thread loader([&config_promise]() {
Config c = load_config_from_disk();
config_promise.set_value(std::move(c));
});
// 워커 스레드들 (각자 설정 복사본 사용)
std::vector<std::thread> workers;
for (int i = 0; i < 4; ++i) {
workers.emplace_back([config_future, i]() { // 값 캡처
const Config& cfg = config_future.get(); // 각 스레드에서 한 번씩
std::cout << "Worker " << i << " got config\n";
do_work(cfg);
});
}
loader.join();
for (auto& w : workers) w.join();
return 0;
}
왜 shared_future를 사용하나?: 설정 파일을 한 번만 로드하며, 여러 워커가 동일한 설정을 사용합니다. future를 사용하면 한 워커만 결과를 받을 수 있지만, shared_future를 사용하면 모든 워커가 결과를 받을 수 있습니다.
const Config& cfg = config_future.get();는 설정을 복사하지 않고 공유 상태 안의 Config를 가리키는 참조를 받습니다. 여러 워커가 같은 Config 객체를 읽기만 하므로 안전하고, 참조는 워커가 캡처한 config_future 복사본이 살아 있는 동안 유효합니다. 만약 워커가 설정을 바꿔 써야 한다면 Config cfg = config_future.get();로 복사본을 만들어야 하고, 공유 상태 안의 값을 const_cast로 고치는 것은 다른 워커와의 데이터 레이스입니다. 설정 로드가 실패할 수 있다면 loader 스레드도 앞 절처럼 try/catch로 set_exception을 해 줘야, 워커들이 영원히 기다리지 않고 예외를 받습니다.
실전 예시: shared_future로 캐시/지연 계산 공유
한 번만 계산하며, 여러 호출자가 결과를 공유하는 패턴에도 쓸 수 있습니다. (실제 캐시는 락이나 atomic 등으로 보강하는 경우가 많지만, shared_future가 “한 번만 채워지는 값” 역할을 할 수 있음.)
std::promise<HeavyResult> p;
std::shared_future<HeavyResult> sf = p.get_future().share();
std::thread producer([&p]() { p.set_value(compute_heavy()); });
std::vector<std::thread> consumers;
for (int i = 0; i < 3; ++i) {
consumers.emplace_back([sf]() { use(sf.get()); });
}
producer.join();
for (auto& c : consumers) c.join();
자주 발생하는 문제
1. get() 중복 호출
std::shared_future<int> sf = ...;
// shared_future는 같은 객체에서 get()을 여러 번 불러도 됨
int r1 = sf.get(); // OK
int r2 = sf.get(); // OK: 같은 값 (future였다면 두 번째 호출이 실패)
// ✅ 그래도 반복 사용할 값은 변수에 받아 두는 편이 읽기 쉬움
int result = sf.get();
use(result);
use(result); // OK
흔한 오해: “get()은 한 번만”이라는 규칙은 std::future의 것입니다. std::future::get()은 결과를 꺼내 가며 future를 무효로 만들기 때문에 두 번째 호출이 실패하지만, shared_future::get()은 상태를 바꾸지 않아 같은 객체에서 반복 호출해도 됩니다. 이 차이가 헷갈리는 경우는 대개 std::future를 쓰다가 두 번째 get()에서 std::future_error: No associated state를 만난 뒤, 그 규칙을 shared_future에도 그대로 적용해서입니다.
권장 사용 형태:
// 패턴 1: 결과를 지역 변수(참조)로 받아 재사용
void worker(std::shared_future<Config> sf) {
const Config& cfg = sf.get(); // 한 번만 호출
for (int i = 0; i < 10; ++i) {
use_config(cfg); // 저장된 결과 재사용
}
}
// 패턴 2: 여러 스레드가 각자 복사본 사용
std::shared_future<int> sf = ...;
std::thread t1([sf]() { int r = sf.get(); }); // 복사본 1
std::thread t2([sf]() { int r = sf.get(); }); // 복사본 2
2. promise를 너무 늦게 set_value/set_exception
// ❌ 예외 발생 시 set을 안 하면 데드락
void bad_producer(std::promise<int>& p) {
try {
int result = compute();
p.set_value(result);
} catch (...) {
// set_exception을 안 함!
}
// p는 참조로 받았으므로 여기서 파괴되지 않음 → 대기자는 영원히 블록
}
// ✅ 모든 경로에서 set 호출
void good_producer(std::promise<int>& p) {
try {
int result = compute();
p.set_value(result);
} catch (...) {
p.set_exception(std::current_exception());
}
}
bad_producer가 위험한 이유는 promise의 수명과 관련 있습니다. promise가 값을 설정하지 않은 채 파괴되면 표준은 공유 상태에 broken_promise 에러를 저장해 대기자들이 예외를 받게 해 주지만, 이 예제처럼 promise를 참조로 받아 호출자가 계속 들고 있으면 파괴가 일어나지 않아 모든 대기자가 영원히 멈춥니다. 멈춘 스레드는 CPU를 쓰지 않아 모니터링에서도 잘 보이지 않고, 프로세스 종료 시 join에서 걸려 서비스가 내려가지 않는 증상으로 발견되는 경우가 많습니다. 대기하는 쪽에서 wait_for로 상한을 두는 것(아래 패턴 3)이 이런 버그의 피해를 줄이는 방어책입니다.
실무 패턴 - RAII로 보장:
// promise가 항상 set되도록 보장
template<typename T>
class PromiseGuard {
std::promise<T>& promise_;
bool set_ = false;
public:
explicit PromiseGuard(std::promise<T>& p) : promise_(p) {}
~PromiseGuard() {
if (!set_) {
promise_.set_exception(
std::make_exception_ptr(std::runtime_error("Promise not set"))
);
}
}
void set_value(T value) {
promise_.set_value(std::move(value));
set_ = true;
}
void set_exception(std::exception_ptr e) {
promise_.set_exception(e);
set_ = true;
}
};
void safe_producer(std::promise<int>& p) {
PromiseGuard guard(p);
int result = compute();
guard.set_value(result); // 예외 발생해도 소멸자가 set_exception 호출
}
3. future 대신 shared_future 필요 여부
// ❌ 불필요하게 shared_future 사용
std::shared_future<int> sf = std::async([]{ return 42; }).share();
int result = sf.get(); // 한 스레드만 사용하면 future로 충분
// ✅ future로 충분
std::future<int> f = std::async([]{ return 42; });
int result = f.get();
// ✅ 여러 스레드가 사용할 때만 shared_future
std::shared_future<int> sf = std::async([]{ return 42; }).share();
std::thread t1([sf]() { use(sf.get()); });
std::thread t2([sf]() { use(sf.get()); });
성능 고려사항: shared_future는 참조 카운팅 오버헤드가 있으므로, 단일 스레드에서만 사용한다면 future가 더 효율적입니다.
PromiseGuard는 소멸자에서 set_exception을 호출하므로, 스택 되감기 중에 원래 던져진 예외의 정보는 잃고 “Promise not set”이라는 일반 메시지만 전달됩니다. 원인을 보존하려면 위의 good_producer처럼 catch (...) 블록 안에서 std::current_exception()을 넘기는 방식이 낫고, 가드는 “어떤 경로로든 반드시 설정한다”는 최후의 안전망으로 두는 것이 적절합니다. 또 set_value 자체가 예외를 던질 수 있는 타입(복사 중 예외)이라면 set_ 플래그를 설정하기 전에 예외가 나서 소멸자가 다시 set_exception을 시도하고, 이미 값이 설정되었다면 promise_already_satisfied가 소멸자에서 던져져 std::terminate로 이어질 수 있다는 점도 고려해야 합니다.
4. 참조 캡처로 같은 객체를 공유
std::shared_future<int> sf = ...;
// ❌ 참조 캡처: 같은 객체를 여러 스레드가 공유
std::thread t1([&sf]() { int r = sf.get(); });
std::thread t2([&sf]() { int r = sf.get(); }); // 같은 객체 공유: 피할 것
// ✅ 값 캡처: 각 스레드가 복사본 사용
std::thread t1([sf]() { int r = sf.get(); });
std::thread t2([sf]() { int r = sf.get(); }); // OK
실무 패턴
패턴 1: 브로드캐스트 알림
// 여러 스레드가 특정 이벤트를 대기
class EventBroadcaster {
std::promise<void> event_promise_;
std::shared_future<void> event_future_;
public:
EventBroadcaster()
: event_future_(event_promise_.get_future().share()) {}
void trigger() {
event_promise_.set_value(); // 모든 대기 스레드 깨움
}
std::shared_future<void> subscribe() {
return event_future_; // 복사본 반환
}
};
// 사용
EventBroadcaster broadcaster;
std::thread t1([f = broadcaster.subscribe()]() { f.wait(); do_work(); });
std::thread t2([f = broadcaster.subscribe()]() { f.wait(); do_work(); });
broadcaster.trigger(); // 모든 스레드 동시 시작
shared_future<void>를 이용한 브로드캐스트는 한 번만 울리는 신호라는 점이 조건 변수와 다릅니다. 한 번 set_value()한 promise는 다시 설정할 수 없어서 trigger()를 두 번 호출하면 std::future_error(promise_already_satisfied)가 던져집니다. 반대로 trigger() 이후에 subscribe()한 스레드도 wait()가 즉시 반환되므로, “신호를 놓치는” 조건 변수의 고전적인 버그(lost wakeup)는 생기지 않습니다. 반복되는 이벤트가 필요하면 C++20의 std::latch(한 번)나 std::barrier(반복), 또는 조건 변수가 맞는 도구입니다.
패턴 2: 여러 단계 파이프라인
// 단계별로 결과를 공유하는 파이프라인
std::promise<Data> stage1_promise;
auto stage1_future = stage1_promise.get_future().share();
std::promise<ProcessedData> stage2_promise;
auto stage2_future = stage2_promise.get_future().share();
// Stage 1: 데이터 로드
std::thread loader([&]() {
stage1_promise.set_value(load_data());
});
// Stage 2: 여러 프로세서가 Stage 1 결과 대기
std::vector<std::thread> processors;
for (int i = 0; i < 3; ++i) {
processors.emplace_back([stage1_future, i]() {
auto data = stage1_future.get();
process(data, i);
});
}
// Stage 3: 최종 결과 대기
std::thread finalizer([stage2_future]() {
auto result = stage2_future.get();
finalize(result);
});
이 파이프라인은 구조만 보여 주는 뼈대라, 그대로 두면 stage2_promise에 값을 설정하는 코드가 없어 finalizer가 끝나지 않고, 스레드들을 join하지 않은 채 스코프를 벗어나면 std::thread 소멸자가 std::terminate를 호출합니다. 실제로는 프로세서들의 결과를 모아 stage2_promise.set_value(...)하는 단계가 있어야 하고, stage1_future.get()으로 받은 auto data는 공유 상태의 값을 복사하므로, 데이터가 크다면 const auto&로 받아야 복사 비용을 피할 수 있습니다.
패턴 3: 타임아웃과 함께 사용
// shared_future와 wait_for로 타임아웃 처리
std::shared_future<int> sf = ...;
std::thread worker([sf]() {
using namespace std::chrono;
if (sf.wait_for(5s) == std::future_status::ready) {
int result = sf.get();
std::cout << "Got result: " << result << '\n';
} else {
std::cout << "Timeout!\n";
}
});
wait_for의 반환값은 ready와 timeout 두 가지만이 아닙니다. sf가 std::async의 기본 실행 정책으로 만든 future에서 왔다면, 구현이 지연 실행(std::launch::deferred)을 골랐을 때 wait_for는 작업을 실행하지 않고 즉시 std::future_status::deferred를 돌려줍니다. 위 코드는 이를 timeout과 같은 분기로 처리해 항상 “Timeout!”을 출력하게 됩니다. 이런 혼란을 피하려면 std::async(std::launch::async, ...)로 정책을 명시하거나, deferred를 별도로 확인해 그 경우에는 get()으로 직접 실행하게 해야 합니다. 또 타임아웃이 나도 작업 자체는 취소되지 않고 계속 실행된다는 점, 그리고 std::async가 반환한 future(여기서는 공유 상태)가 마지막으로 파괴될 때 작업 완료를 기다리며 블록된다는 점도 함께 기억해야 합니다.
자주 묻는 질문
Q: shared_future와 future의 가장 큰 차이는?
A: future는 이동만 가능하고 get()을 한 번만 호출할 수 있지만, shared_future는 복사 가능하고 get()을 몇 번이든 호출할 수 있습니다. 여러 스레드가 같은 결과를 읽을 때 shared_future를 사용합니다.
Q: shared_future는 스레드 안전한가요?
A: 스레드마다 자기 복사본을 가지면 안전합니다. 각 스레드가 값 캡처로 복사본을 가지면 안전하게 get()을 호출할 수 있습니다. 같은 shared_future 객체를 여러 스레드가 참조로 공유하는 것은 표준 보장 범위 밖이고, 대입·이동이 섞이면 데이터 레이스가 되므로 피해야 합니다.
Q: get()을 여러 번 호출하려면?
A: shared_future라면 그냥 여러 번 호출하면 됩니다. 매번 같은 값에 대한 const 참조를 돌려줍니다. 한 번만 호출할 수 있는 것은 std::future이므로, future에서 여러 번 읽어야 한다면 .share()로 바꾸거나 첫 결과를 변수에 저장해 두세요.
Q: promise를 set하지 않으면?
A: promise가 값을 설정하지 않고 소멸되면 공유 상태에 std::future_error(broken_promise)가 저장되어, get() 대기 중인 모든 스레드에서 예외가 던져집니다. 반대로 promise가 소멸되지 않고 살아 있으면 대기자는 영원히 블록됩니다. 모든 경로에서 set_value() 또는 set_exception()을 호출하세요.
Q: 언제 future 대신 shared_future를 사용하나요?
A: 여러 스레드가 같은 비동기 결과를 읽어야 할 때만 shared_future를 사용하세요. 단일 스레드만 결과를 받으면 future로 충분합니다.
관련 글: future와 promise, async 비동기 실행, 스레드 기초.
같이 보면 좋은 글
- C++ promise와 future: 스레드 간 결과 전달, std::async 실행 정책, shared_future
- C++ std::async와 launch 정책: future, deferred 실행, 흔한 함정
- C++ std::thread 입문 | join 누락·디태치 남용 등 자주 하는 실수 3가지와 해결법
- C++ packaged_task
- C++ atomic: 락 없는 카운터, 메모리 순서, 언제 mutex 대신 쓰나
- C++ Memory Order