본문으로 건너뛰기
Folly Code Review · 64/89

folly::coro 개요 — production C++20 코루틴 어댑터

· Hawk · 5분 읽기

한 줄 요약: C++20 코루틴은 언어 키워드만 표준이다. Task, awaiter, executor 통합, cancellation 같은 실행 모델은 라이브러리가 채워야 한다. folly::coro는 production code에서 그 빈자리를 가장 빨리 메운 구현이다.

#동기

C++20이 도입한 것은 co_await, co_yield, co_return 세 키워드와 coroutine_handle<>, promise_type 컨셉 정도다. 정작 “코루틴으로 무엇을 표현할 것인가”는 표준에 없다.

표준이 준 것표준이 안 준 것
co_await/co_yield/co_returnTask<T>
coroutine_handle<P>Generator<T>
promise_type 컨셉AsyncGenerator<T>
suspend_always/suspend_neverexecutor 바인딩
collectAll / when_all
cancellation 전파
coroutine-aware Mutex/Baton
blocking wait

결과적으로 같은 키워드로 cppcoro, folly::coro, stdexec, libunifex, asio::awaitable 등 겹치지 않는 라이브러리가 동시에 존재한다. Meta는 Folly Futures를 운영하다가 코루틴 도입 시점부터 자체 어댑터를 키웠다. 이게 folly::coro다.

#위치 — 다른 라이브러리와의 비교

라이브러리시작 시점주력 도메인상태
cppcoro2017, Lewis Baker학습/실험용 코루틴 toolkit유지보수 정체
libunifex2019, Meta + communitysender/receiver의 reference impl활발하나 lower-level
stdexec2022, NVIDIA + communityC++26 P2300 후보 구현표준 후보
asio::awaitable2020, Chris Kohlhoffnetworking + 코루틴매우 안정
folly::coro2019, MetaFutures와 호환되는 production asyncfbcode에서 매일 검증

folly::coro의 정체성은 Futures 기반 기존 API와 단방향 ↔ 양방향 변환이 가능한 코루틴 어댑터다. 새 코드는 Task<T>로 짜고, 기존 SemiFuture<T> 반환 함수도 그대로 co_await 할 수 있다.

folly::coro::Task<std::string> Fetch(std::string url);
folly::SemiFuture<int> LegacyParse(std::string body);
folly::coro::Task<int> Pipeline(std::string url) {
auto body = co_await Fetch(url); // Task
int n = co_await LegacyParse(body); // SemiFuture
co_return n;
}

이 경계 호환성이 production migration에서 결정적이었다.

#주요 타입

folly::coro::Task<T> — lazy single-shot, executor에 schedule
folly::coro::AsyncGenerator<T> — async stream (co_yield)
folly::coro::Generator<T> — sync stream (co_yield) — 일반 코루틴 generator
folly::coro::Baton — coroutine-aware 1-shot notification
folly::coro::Mutex — coroutine-aware mutex
folly::coro::SharedMutex — RW mutex
folly::coro::Semaphore — counting semaphore
folly::coro::Promise<T> — manual promise (Future의 Promise와 유사)
folly::coro::AsyncScope — fire-and-forget 안전판
folly::coro::CancellationToken — 협력적 취소

각각은 다음 절들에서 개별로 다룬다.

#API 한눈에

#include <folly/coro/Task.h>
#include <folly/coro/BlockingWait.h>
#include <folly/coro/Collect.h>
#include <folly/executors/CPUThreadPoolExecutor.h>
folly::coro::Task<int> Compute(int x) {
// 다른 Task await
int y = co_await Helper(x);
co_return x * y;
}
folly::coro::Task<std::vector<int>> ParallelCompute() {
auto [a, b, c] = co_await folly::coro::collectAll(
Compute(1), Compute(2), Compute(3));
co_return std::vector{a, b, c};
}
int main() {
folly::CPUThreadPoolExecutor pool(4);
auto result = folly::coro::blockingWait(
ParallelCompute().scheduleOn(&pool));
// result = [1, 2, 9]
}

세 가지 패턴이 보인다.

  1. Task<T> 함수는 일반 함수처럼 정의한다. co_return이 일반 return 자리.
  2. 다른 Taskco_await해서 합성한다.
  3. collectAll로 fan-out, blockingWait로 sync 경계 연결.

#왜 Folly가 별도로 만들었나

C++20 도입 시점(2020)에는 다음이 없었다.

  • 표준 Task<T> 타입.
  • executor와 코루틴의 표준 통합.
  • senders/receivers (P2300은 아직 후보).
  • 표준 cancellation 모델.

Meta는 이미 수십만 곳에서 folly::SemiFuture를 쓰고 있었고, 코루틴이 callback 지옥을 해소할 가능성이 명백했다. 표준이 따라올 때까지 기다리지 않고 다음 결정을 했다.

  1. Task<T>SemiFuture<T>와 변환 가능하게 설계 — migration 부담 최소화.
  2. 모든 awaitable이 executor에 명시적으로 schedule 되도록 강제 — implicit “current thread” 회피.
  3. CancellationToken을 매개로 cancellation을 코루틴 트리에 전파 — sender/receiver의 stop token과 형식적으로 동등.

결과적으로 stdexec/P2300이 표준으로 들어와도 folly::coro API는 크게 깨지지 않을 설계가 됐다. 두 모델이 sender adapter로 양방향 변환 가능하다.

#작은 실전 — RPC pipeline

folly::coro::Task<UserProfile> GetUserProfile(UserId id) {
// 동시에 셋을 시작
auto userTask = userService_->getUser(id);
auto prefsTask = prefsService_->getPrefs(id);
auto avatarTask = avatarService_->getAvatar(id);
auto [user, prefs, avatar] = co_await folly::coro::collectAll(
std::move(userTask),
std::move(prefsTask),
std::move(avatarTask));
co_return UserProfile{std::move(user), std::move(prefs), std::move(avatar)};
}

같은 패턴을 folly::Future로 짜면 collect(...) + .thenValue([](auto&& tup) { ... }) + lambda capture가 줄줄이 따라온다. 코루틴은 호출 구조와 데이터 흐름이 같은 모양이라는 게 가장 큰 가독성 이득.

#suspend / resume 모델

C++ coroutine은 stackless다. 호출 스택을 쥐고 있지 않고, 필요한 상태만 heap frame에 저장하고 caller에 반환한다.

Coroutine suspend / resume

suspend 시점에 frame에 resume 주소와 locals를 저장하고 caller 스택은 즉시 풀린다. resume 시 frame을 다시 활성화 — 그래서 같은 스레드 보장이 없고, 따라서 executor 바인딩이 필수가 된다.

#내부 구현 개념

// folly/coro/Task.h 의 약식
template <class T>
class Task {
public:
class promise_type {
public:
Task<T> get_return_object() noexcept;
suspend_always initial_suspend() noexcept; // lazy start
auto final_suspend() noexcept; // resume awaiter
void return_value(T v) noexcept;
void unhandled_exception() noexcept;
// executor binding
folly::Executor::KeepAlive<> executor_;
// exception 보관
folly::Try<T> result_;
};
// awaiter: co_await Task<T>의 동작
auto operator co_await() && noexcept;
};

핵심 결정 두 가지.

  • lazy start: initial_suspend()suspend_always다. Task<int> t = Compute()만으로는 실행이 시작되지 않는다. co_await 또는 scheduleOn(executor)이 trigger.
  • executor 강제: top-level Task는 반드시 scheduleOn(executor) 또는 부모 Task의 executor로 실행된다. “어디서도 실행 안 됨” 상태를 컴파일 타임에 가깝게 막는다.

#std와의 비교

항목std (C++20)folly::corostdexec
Task 타입없음Task<T>sender
executor없음명시 필수scheduler
cancellation없음CancellationTokenstop_token
AsyncGenerator없음있음별도 모델
Mutex/Baton없음있음직접 짜야
Futures 호환N/ASemiFuture 양방향adapter 필요
표준 후보N/A아님 (Meta 내부 표준)P2300

folly::coro는 표준이 되려는 야심이 없다. 지금 production에서 쓰는 도구다.

#코드 리뷰 포인트

  • Task를 반환하는 함수가 호출자 손에서 co_awaitscheduleOn도 안 받으면 영원히 실행되지 않는다. compile-time 경고 없다. 리뷰에서 잡아야 한다.
  • top-level entry point에서만 blockingWait. 라이브러리 내부에서 blockingWait 호출은 deadlock 원천이다.
  • co_await 안에서 lambda capture가 reference면 코루틴 suspend 동안 dangling 가능성. value capture가 기본.
  • AsyncScope 없이 co_spawn/detached 패턴을 흉내내면 종료 시 미완료 코루틴이 남는다.

#자주 보는 안티패턴

// 1. Task를 호출만 하고 await도 schedule도 안 함
void Caller() {
Compute(1); // 영원히 실행되지 않음
}
// 2. 라이브러리 함수 안에서 blockingWait
int Helper() {
return folly::coro::blockingWait(SomeTask()); // 호출자가 이미 coro면 deadlock 위험
}
// 3. reference capture가 lambda 외부 수명보다 김
folly::coro::Task<int> Bad(int& x) {
co_return co_await Helper(x); // Bad() 반환 후 x가 dangling이면 끝
}

#정리

  • C++20 코루틴은 키워드만 표준이다. Task, executor, cancellation은 라이브러리 책임이다.
  • folly::coroSemiFuture와의 양방향 호환을 유지하며 production async를 표현한다.
  • 모든 Task는 lazy start이고 executor 바인딩이 필수다.
  • 다음 절들에서 Task, AsyncGenerator, blockingWait, Baton/Mutex를 개별로 본다.
  • stdexec/P2300이 표준화돼도 sender adapter로 변환 가능한 형태로 설계됐다.

#다음 편

Part 15-02: folly::coro::Task에서 가장 자주 쓰는 타입을 본격적으로 본다.

#관련 항목

Folly Code Review · 65 of 89

  1. 1 Folly Code Review — Meta의 production-grade C++ 라이브러리 코드 분석
  2. 2 Folly 개요 — Meta가 production에서 검증한 utility 모음 분석
  3. 3 Folly vs Abseil 철학 비교 — performance-first vs std-compatible
  4. 4 Folly 빌드와 fbcode 환경 — monorepo의 그림자
  5. 5 Folly API stability 정책 — 어떤 보장도 없다는 솔직함
  6. 6 Folly production validation 문화 — peta-scale에서 단련된 코드
  7. 7 folly::Future 분석 — std::future의 한계를 넘는 composable async
  8. 8 folly::Promise·makeFuture — Future를 만드는 두 길
  9. 9 folly::SemiFuture vs Future — executor binding의 명시화
  10. 10 folly::Future thenValue·thenError·thenTry — continuation 체인 분석
  11. 11 folly::collect·collectAll·collectAny — fan-in 패턴 분석
  12. 12 folly::Future retry·window·via — 제어 흐름 조합자
  13. 13 folly::fibers 분석 — M:N stackful coroutine
  14. 14 folly::InlineExecutor — 호출자 thread에서 즉시 실행
  15. 15 folly::CPUThreadPoolExecutor — CPU-bound 작업의 표준 thread pool
  16. 16 folly::IOThreadPoolExecutor — libevent 기반 I/O pool
  17. 17 folly::ManualExecutor — 결정적 테스트를 위한 수동 진행
  18. 18 folly::EventBase 분석 — libevent 이벤트 루프의 핵심
  19. 19 folly::IOBuf 분석 — zero-copy buffer chain의 기본 단위
  20. 20 folly::IOBufQueue — chain의 push/pull 추상화
  21. 21 folly::io::Cursor·RWCursor — chain 위의 stream
  22. 22 folly Zero-copy 패턴 — IOBuf로 ScatterGather I/O 표현
  23. 23 folly::IOBuf shared semantics — clone·unshare·takeOwnership
  24. 24 folly::FBString 분석 — SSO + COW 구현
  25. 25 folly의 fmt::format 통합 — 모던 포맷팅 채택
  26. 26 folly::StringPiece — string_view 호환 분석
  27. 27 folly Join·Split utilities — 문자열 분해와 결합
  28. 28 folly::to·tryTo — text↔num 변환 분석
  29. 29 folly Conv Customization — 사용자 타입 지원
  30. 30 folly Conv 성능 비교 — sprintf·stringstream 대비
  31. 31 folly::F14ValueMap vs std::unordered_map
  32. 32 folly::F14NodeMap — stable pointer가 필요할 때
  33. 33 folly::F14VectorMap — cache-friendly iteration
  34. 34 folly::F14FastMap — auto-select 동작
  35. 35 folly F14 internals — SIMD probing 메커니즘
  36. 36 folly::small_vector — inline storage 분석
  37. 37 folly::FixedString — compile-time string
  38. 38 folly::AtomicHashMap — lock-free read 분석
  39. 39 folly::ConcurrentHashMap — sharded 동시 해시 맵
  40. 40 folly::EvictingCacheMap — LRU 구현 분석
  41. 41 folly::Synchronized — lock wrapper 패턴
  42. 42 folly::SharedMutex 분석
  43. 43 folly::Baton — one-shot wait 동기화
  44. 44 folly::RWSpinLock 분석
  45. 45 folly::PicoSpinLock — 1-byte spinlock
  46. 46 folly::ProducerConsumerQueue — SPSC 큐 분석
  47. 47 folly::MPMCQueue — multi-producer multi-consumer
  48. 48 folly::UnboundedQueue — 동적 크기 lock-free
  49. 49 folly::fibers::Channel — Go-like channel
  50. 50 folly::dynamic — JSON-like dynamic type 분석
  51. 51 folly JSON conversion — toJson·parseJson
  52. 52 folly dynamic ↔ struct — manual marshaling
  53. 53 folly dynamic Visitor pattern — type별 분기
  54. 54 folly::Singleton vs Meyers/static — 왜 Folly의 Singleton인가
  55. 55 folly::SingletonVault 분석 — 등록·소멸·의존성
  56. 56 folly::Singleton try_get·try_get_fast — TLS-cached 접근
  57. 57 folly::ExceptionWrapper — type-erased exception holder
  58. 58 folly::ScopeGuard·SCOPE_EXIT — RAII cleanup
  59. 59 folly::Optional vs std::optional
  60. 60 folly::Function vs std::function
  61. 61 folly::Lazy — 지연 초기화 wrapper
  62. 62 folly Meta 스타일 code review 패턴
  63. 63 folly anti-patterns — 잘못 쓰면 std보다 느림
  64. 64 folly vs std 선택 기준 분석
  65. 65 folly::coro 개요 — production C++20 코루틴 어댑터
  66. 66 folly::coro::Task — lazy single-shot 코루틴
  67. 67 folly::coro::AsyncGenerator — 비동기 스트림
  68. 68 folly coro blockingWait·collectAll — 동기 경계와 fan-in
  69. 69 folly::coro::Baton·Mutex — 코루틴-aware 동기화
  70. 70 folly::Expected — 결과 또는 오류
  71. 71 folly::Try — Future 결과 wrapper
  72. 72 folly::Try vs Expected 선택 기준
  73. 73 folly::Range — 일반 iterator pair
  74. 74 folly::Uri — URL 파서
  75. 75 folly Fingerprint64·128 — 분산 hash
  76. 76 folly SpookyHashV2 — fast non-crypto hash
  77. 77 folly::Init — main() 부트스트랩
  78. 78 folly::Indestructible — global lifetime 패턴
  79. 79 folly::MicroLock — 1-byte 락
  80. 80 folly::MicroSpinLock — 가장 좁은 spin lock
  81. 81 folly::format — legacy formatter 분석
  82. 82 folly::demangle — typeid 디망글링
  83. 83 folly::DynamicConverter — dynamic ↔ struct
  84. 84 folly::RecordIO — append-only 로그 파일 포맷
  85. 85 folly::io::Compression — zstd·lz4·snappy wrapper
  86. 86 folly::AsyncIO — io_uring·Linux AIO
  87. 87 folly::CancellationToken — 코루틴·Future 취소 전파
  88. 88 folly::observer — hot config의 atomic refresh
  89. 89 fbcode 패턴 모음 — folly 사용의 실전