아래는 네가 올린 “제미나이 버전”을 같은 흐름으로 유지하되, 게임 서버 관점에서 빠지기 쉬운 함정(정확한 의미/수명/예외/정책/실전 패턴)까지 보강해서 만든 완전판 학습 자료야.
(문서 톤은 강의처럼 자연스럽되, 기술 문서로 바로 쓸 수 있게 정제했고, 코드도 “실전 안전 버전”으로 다듬었어.)


[심화 완전판] C++ 비동기 프로그래밍: Future · Promise · Packaged_task

(게임 서버 관점: “단발성 이벤트”를 가장 간결하게 처리하는 법)


  1. 개요: 왜 이게 필요한가?

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회) 결과 수집을 “락 중심 구조” 없이 깔끔하게 한다.


  1. 가장 쉬운 비동기: std::async + std::future

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 동작 (정확한 의미)

  1. async(...) 호출 시점에 작업이 스케줄됨
  1. 메인 스레드는 바로 다음 코드로 진행 가능
  1. future.get()은 “결과 수령”이자 “동기화 지점”

아직 미완료면 완료될 때까지 블로킹

완료면 즉시 반환


  1. std::launch 옵션 3종 (비동기 ≠ 멀티쓰레드 핵심)

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)가 안전


  1. 완료 여부만 살짝 보고 싶을 때: wait_for / future_status

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. std::future의 매우 중요한 규칙 3가지 (실수 방지)

규칙 1) future.get()은 딱 1번

get()은 값을 “수령”하면서 future를 소진한다.
두 번 호출하면 예외가 날 수 있다.

규칙 2) 예외도 future로 전파된다

비동기 작업에서 예외가 발생하면:

호출 시점이 아니라

get() 호출 시점에 예외가 다시 던져진다

즉, get()은 “결과 수령 + 예외 수령”이다.

규칙 3) 결과를 여러 군데서 기다려야 하면 shared_future

단발성 future는 한 명만 받을 수 있다.
여러 소비자가 필요하면:

std::shared_future<int64_t> sf = fut.share();


  1. 약속된 미래: std::promise (값을 “직접” 넣는다)

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 파이프다.


  1. 실행의 포장: std::packaged_task (작업 객체로 만들기)

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로 깔끔하게 회수된다.


  1. 3종 비교: “제어권(Level of Control)”로 완전 정리

도구 한 줄 정의 강점 주 사용처

std::async + future “함수 맡기고 결과 받기” 가장 간단, 빠른 구현 단발성 병렬 로딩, 간단 비동기
packaged_task + future “함수를 태스크로 포장해 원하는 스레드에서 실행” 실행 스레드/시점 통제 스레드 풀/잡 시스템
promise + future “내가 값을 직접 넣어 미래 결과를 완성” 값 세팅 자유, 예외 전달도 깔끔 이벤트성 결과 전달, 커스텀 완료 신호

핵심 요약

“간단히 끝낼래” → async

“잡 시스템/워커로 던질래” → packaged_task

“결과를 내가 컨트롤해야 해(조건/실패/중간결과)” → promise


  1. 게임 서버 실무 예시 (강의 흐름 강화)

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()이 “완료 대기 + 결과 수령”을 한 번에 처리한다.


  1. 가장 자주 터지는 함정/주의사항 (완전 실전 체크리스트)

  2. get() 2번 호출 금지 (단발성 수령)

  1. launch::deferred는 병렬이 아닐 수 있음
  1. 비동기 작업의 예외는 get()에서 터짐 → try/catch는 get 주변에
  1. 여러 곳에서 결과를 기다리면 shared_future 고려
  1. “작업을 많이 뿌리는 구조”는 async 난사보다 스레드 풀 + packaged_task가 설계적으로 맞는 경우가 많다

  1. Takeaway (이 문서의 결론)

Condition Variable: 지속형(반복) 동기화에 최적

Future 삼형제: 단발성 비동기 결과 수집에 최적

가장 쉬운 선택은 std::async

제어가 필요해지면 packaged_task, 값 세팅 자유가 필요하면 promise


원하면 내가 이 자료를 “진짜 게임 서버에 바로 붙이는 템플릿”으로 한 단계 더 업그레이드해줄게.
예를 들어:

std::packaged_task 기반 작은 스레드 풀 + 작업 큐 + future 결과 회수

“부팅 로딩 병렬화”를 타임아웃/실패/로그/예외 전파까지 포함한 운영 코드

원하는 쪽(스레드 풀 vs 부팅 로딩 템플릿)만 말해줘.

profile
李家네_공부방

0개의 댓글