아래는 네가 올린 “제미나이 버전”을 같은 흐름으로 유지하되, 게임 서버 관점에서 빠지기 쉬운 함정(정확한 의미/수명/예외/정책/실전 패턴)까지 보강해서 만든 완전판 학습 자료야.
(문서 톤은 강의처럼 자연스럽되, 기술 문서로 바로 쓸 수 있게 정제했고, 코드도 “실전 안전 버전”으로 다듬었어.)
[심화 완전판] C++ 비동기 프로그래밍: Future · Promise · Packaged_task
(게임 서버 관점: “단발성 이벤트”를 가장 간결하게 처리하는 법)
1) Condition Variable의 장점과 “범위 밖”
std::condition_variable은 지속형 Producer–Consumer에 최적이다.
패킷 큐/잡 큐처럼 계속 들어오고 계속 처리하는 구조
“데이터가 없으면 잠들고, 들어오면 깨어나서 처리”
하지만 단발성 이벤트에는 과하다.
“이 함수 좀 실행해두고 나중에 결과만 주세요”
“부팅 시 데이터 로딩 여러 개 병렬로 돌리고 다 끝나면 시작”
“DB 쿼리/파일 로딩 1회성 작업 완료만 기다리고 싶다”
이런 상황에서 매번 mutex + cv + wait + notify로 구조를 만들면 코드가 커지고 실수 포인트가 늘어난다.
2) 닭 잡는데 소 잡는 칼을 쓸 필요 없다
C++11은 단발성 비동기 결과 수집을 위해 Future 삼형제를 넣어줬다.
std::future : “미래에 결과 받을 수 있는 티켓”
std::promise : “내가 나중에 값을 채워주겠다는 약속”
std::packaged_task : “함수를 ‘작업 객체’로 포장해서 future와 연결”
핵심: 단발성(1회) 결과 수집을 “락 중심 구조” 없이 깔끔하게 한다.
2.1 개념
std::async : “이 함수 비동기로 실행해줘”
std::future : “그 결과(T)를 나중에 받을게”
2.2 예제 (실전 안전 버전)
#include
#include
#include
int64_t Calculate()
{
int64_t sum = 0;
for (int64_t i = 0; i < 100'000; ++i)
sum += i;
return sum;
}
int main()
{
// 명시적으로 async 정책을 주면 “진짜 별도 스레드”로 실행됨
std::future<int64_t> fut = std::async(std::launch::async, Calculate);
std::cout << "Main thread doing other work...\n";
// 결과가 필요해지는 순간: 준비 안 됐으면 여기서 block
int64_t result = fut.get();
std::cout << "Result: " << result << "\n";
}
2.3 Step-by-step 동작 (정확한 의미)
아직 미완료면 완료될 때까지 블로킹
완료면 즉시 반환
3.1 std::launch::async
별도 스레드에서 즉시 실행(진짜 병렬 실행)
“서버 부팅 로딩 병렬화” 같은 데 직관적
3.2 std::launch::deferred (Lazy Evaluation)
실행을 “예약만” 해두고
get()/wait() 호출 시점에, 호출한 스레드에서 실행
즉, “비동기처럼 보이지만 멀티스레드가 아닐 수 있다”
auto fut = std::async(std::launch::deferred, Calculate);
// 아직 Calculate 실행 안 함
auto v = fut.get(); // 여기서 실행 + 완료까지 block
3.3 std::launch::async | std::launch::deferred
구현체(라이브러리)가 상황에 따라 선택
학습/디버깅/서버 코드에서는 대개 명시(추천: async)가 안전
4.1 상태 체크 패턴
#include
using namespace std::chrono_literals;
auto status = fut.wait_for(1ms);
if (status == std::future_status::ready) {
// 완료
} else if (status == std::future_status::timeout) {
// 아직 진행 중
} else if (status == std::future_status::deferred) {
// deferred라 시작 자체가 get/wait까지 밀려있을 수 있음
}
4.2 게임 서버에서의 의미
“부팅 중 로딩 끝났나?”
“프레임/틱 안에서 0~1ms만 확인하고 다음으로 넘기기”
같은 논블로킹 폴링에 쓴다.
규칙 1) future.get()은 딱 1번
get()은 값을 “수령”하면서 future를 소진한다.
두 번 호출하면 예외가 날 수 있다.
규칙 2) 예외도 future로 전파된다
비동기 작업에서 예외가 발생하면:
호출 시점이 아니라
get() 호출 시점에 예외가 다시 던져진다
즉, get()은 “결과 수령 + 예외 수령”이다.
규칙 3) 결과를 여러 군데서 기다려야 하면 shared_future
단발성 future는 한 명만 받을 수 있다.
여러 소비자가 필요하면:
std::shared_future<int64_t> sf = fut.share();
6.1 언제 promise가 필요한가?
async는 “함수 return 값”만 결과로 받을 수 있다.
하지만 실전에선 이런 케이스가 있다:
작업 중 특정 조건이 만족될 때만 값을 전달
return이 아닌 “중간 이벤트 결과” 전달
작업이 실패하면 예외를 전달하고 싶다
“작업 함수 형태”를 자유롭게 유지하고 싶다
이 때 promise가 깔끔하다.
6.2 기본 코드 (강의 흐름 + 안전 보강)
#include
#include
#include
#include
void PromiseWorker(std::promise<std::string>&& p)
{
try {
// 작업 수행...
p.set_value("Secret Message From Thread");
} catch (...) {
// 실패도 future로 전달 가능
p.set_exception(std::current_exception());
}
}
int main()
{
std::promise<std::string> p;
std::future<std::string> f = p.get_future();
std::thread t(PromiseWorker, std::move(p));
try {
std::string msg = f.get(); // 값이 올 때까지 block
std::cout << "Received: " << msg << "\n";
} catch (const std::exception& e) {
std::cout << "Worker failed: " << e.what() << "\n";
}
t.join();
}
6.3 promise/future 관계를 한 줄로
promise = 값을 “넣는 쪽(Producer)”
future = 값을 “받는 쪽(Consumer)”
둘은 1:1 파이프다.
7.1 packaged_task가 필요한 이유
async는 실행 시점/스레드 제어가 제한적이다.
반면 서버에선 “특정 워커 스레드/스레드 풀”에 일을 던지고 싶다.
packaged_task는:
함수를 “태스크 객체”로 포장
결과는 future로 연결
태스크를 원하는 스레드에서 실행 가능
7.2 기본 예제
#include
#include
#include
int64_t Calculate()
{
return 12345;
}
void TaskWorker(std::packaged_task<int64_t(void)>&& task)
{
task(); // 실행하면 결과가 future로 들어감
}
int main()
{
std::packaged_task<int64_t(void)> task(Calculate);
std::future<int64_t> f = task.get_future();
std::thread t(TaskWorker, std::move(task));
std::cout << "Result: " << f.get() << "\n";
t.join();
}
7.3 실전 포인트 (서버 설계 관점)
packaged_task는 “스레드 풀의 Job 타입”으로 쓰기 좋다.
std::function<void()> 형태의 큐에 넣고, 워커가 실행하는 패턴으로 확장 가능하다.
결과는 future로 깔끔하게 회수된다.
도구 한 줄 정의 강점 주 사용처
std::async + future “함수 맡기고 결과 받기” 가장 간단, 빠른 구현 단발성 병렬 로딩, 간단 비동기
packaged_task + future “함수를 태스크로 포장해 원하는 스레드에서 실행” 실행 스레드/시점 통제 스레드 풀/잡 시스템
promise + future “내가 값을 직접 넣어 미래 결과를 완성” 값 세팅 자유, 예외 전달도 깔끔 이벤트성 결과 전달, 커스텀 완료 신호
핵심 요약
“간단히 끝낼래” → async
“잡 시스템/워커로 던질래” → packaged_task
“결과를 내가 컨트롤해야 해(조건/실패/중간결과)” → promise
9.1 서버 부팅: 데이터 로딩 병렬화 (async)
auto aiFut = std::async(std::launch::async, LoadAI);
auto itemFut = std::async(std::launch::async, LoadItems);
auto mapFut = std::async(std::launch::async, LoadMaps);
// 다른 초기화 진행...
aiFut.get();
itemFut.get();
mapFut.get(); // “만남의 장소(합류 지점)”
순차 50초 → 병렬로 줄이는 대표 패턴
9.2 “단발성 완료 통지”는 cv보다 future가 깔끔할 때가 많다
“이 작업 끝나면 알려줘”
“한 번만 결과 받으면 끝”
→ future.get()이 “완료 대기 + 결과 수령”을 한 번에 처리한다.
가장 자주 터지는 함정/주의사항 (완전 실전 체크리스트)
get() 2번 호출 금지 (단발성 수령)
Condition Variable: 지속형(반복) 동기화에 최적
Future 삼형제: 단발성 비동기 결과 수집에 최적
가장 쉬운 선택은 std::async
제어가 필요해지면 packaged_task, 값 세팅 자유가 필요하면 promise
원하면 내가 이 자료를 “진짜 게임 서버에 바로 붙이는 템플릿”으로 한 단계 더 업그레이드해줄게.
예를 들어:
std::packaged_task 기반 작은 스레드 풀 + 작업 큐 + future 결과 회수
“부팅 로딩 병렬화”를 타임아웃/실패/로그/예외 전파까지 포함한 운영 코드
원하는 쪽(스레드 풀 vs 부팅 로딩 템플릿)만 말해줘.