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

folly coro blockingWait·collectAll — 동기 경계와 fan-in

· Hawk · 3분 읽기

한 줄 요약: blockingWait는 코루틴 세계와 동기 세계의 유일한 합법적 경계다. collectAll/collectAllRange는 여러 Task를 fan-out + join 한다. 둘을 잘못 섞으면 deadlock이 정해진 일.

#동기 — 왜 별도 도구가 필요한가

코루틴은 비동기 합성에 강하다. 그러나 두 경계가 있다.

  1. main() 진입점main은 코루틴이 아니다. 어딘가에서 sync로 결과를 기다려야 한다.
  2. legacy 동기 API와의 통합std::vector<int> foo()처럼 sync 시그니처가 코루틴 결과를 호출하려면.

blockingWait가 이 두 자리를 채운다. 한편 production async의 진짜 가치는 병렬 합성에서 나온다. collectAll/collectAllRange가 그 도구.

#blockingWait

#include <folly/coro/BlockingWait.h>
#include <folly/coro/Task.h>
folly::coro::Task<int> Compute();
int main() {
folly::CPUThreadPoolExecutor pool(4);
int v = folly::coro::blockingWait(Compute().scheduleOn(&pool));
std::cout << v << "\n";
}

blockingWait는 현재 thread를 block하고 코루틴이 완료될 때까지 기다린다. 호출 thread 자체는 다른 코루틴을 실행하지 않으므로 executor가 코루틴을 진행시킨다는 사실이 중요하다.

// Bad
int Caller() {
return folly::coro::blockingWait(Compute()); // executor 없음 → 영원히 대기
}

TaskscheduleOn(executor)로 묶지 않으면 lazy start가 풀리지 않는다. blocking thread 혼자서는 코루틴을 깨우지 못한다.

#Deadlock 피하기

// Deadlock 1: 단일 thread pool + blockingWait가 그 pool에서 호출
folly::CPUThreadPoolExecutor pool(1);
folly::coro::Task<int> Inner() { co_return 1; }
folly::coro::Task<int> Outer() {
// Outer가 pool의 유일한 thread에서 실행 중
return folly::coro::blockingWait(Inner().scheduleOn(&pool));
// ↑ Inner도 같은 pool인데 thread 1개라 schedule 불가
}

규칙: blockingWait코루틴/executor 외부에서만 안전하다. main(), test fixture, sync API boundary.

// OK
int main() {
folly::CPUThreadPoolExecutor pool(4);
folly::coro::blockingWait(MyApp().scheduleOn(&pool));
}
// Risk
folly::coro::Task<int> Library() {
return folly::coro::blockingWait(Helper()); // pool 고갈 시 deadlock
}

#collectAll — 정해진 수의 fan-out

#include <folly/coro/Collect.h>
folly::coro::Task<std::tuple<int, std::string, double>> ParallelFetch() {
auto [a, b, c] = co_await folly::coro::collectAll(
FetchInt(),
FetchString(),
FetchDouble());
co_return std::make_tuple(a, b, c);
}

collectAll(t1, t2, t3, ...)은 모든 Task를 동시에 시작하고 전부 완료되면 결과 tuple로 반환한다. 어느 하나라도 실패하면 첫 예외가 throw, 나머지는 폐기.

#collectAllRange — 동적 수

folly::coro::Task<std::vector<int>> ProcessAll(std::vector<int> ids) {
std::vector<folly::coro::Task<int>> tasks;
for (int id : ids) tasks.push_back(ProcessOne(id));
co_return co_await folly::coro::collectAllRange(std::move(tasks));
}

collectAllRange는 range를 받아 std::vector<T>를 반환한다. tuple destructuring이 불가능한 경우(런타임 결정 크기).

#collectAllTry

auto results = co_await folly::coro::collectAllTry(t1, t2, t3);
// results: tuple<Try<R1>, Try<R2>, Try<R3>>
for (auto& r : { results.t0, results.t1, results.t2 }) {
if (r.hasException()) { ... }
}

collectAll이 fail-fast인 반면 collectAllTry는 모든 Task가 완료될 때까지 기다리고 각각의 결과를 Try<T>로 반환. 부분 실패를 허용하는 fan-in.

#collectAny

auto idx = co_await folly::coro::collectAny(a, b, c);
// 가장 먼저 끝난 Task의 인덱스

race 패턴. 가장 빨리 끝나는 결과만 채택, 나머지는 cancellation 신호로 정리(awaitable이 cancel-aware라면).

#내부 — 단순한 합성

// 약식
template <class... Tasks>
folly::coro::Task<std::tuple<typename Tasks::result_t...>>
collectAll(Tasks... tasks) {
// 1. 각 Task를 SemiFuture로 변환
auto futures = std::tuple(std::move(tasks).start()...);
// 2. SemiFuture::collect에 위임
auto results = co_await folly::collectAll(std::move(futures));
// 3. tuple로 묶어 반환
co_return std::move(results);
}

내부적으로 코루틴 Task들이 SemiFuture<T>로 변환되고 기존 folly::collectAll Future 합성에 위임된다. Futures 인프라 위에 코루틴 API를 얹은 형태. 이런 재사용이 Meta가 빠르게 production-ready로 만든 비결.

#awaitable 합성 패턴

folly::coro::Task<Result> FetchWithFallback(Key k) {
auto primary = FetchPrimary(k);
auto secondary = FetchSecondary(k);
// 5초 timeout — 둘 다 안 끝나면 fallback
auto result = co_await folly::coro::co_awaitTry(
folly::coro::timeout(
folly::coro::collectAny(std::move(primary), std::move(secondary)),
std::chrono::seconds{5}));
if (result.hasException()) {
co_return co_await FetchFromCache(k);
}
co_return /* ... */;
}

timeout, co_awaitTry, collectAny가 자유롭게 조합된다. 같은 logic을 callback 또는 future chain으로 짜면 3배 길이.

#std와의 비교

항목표준folly::coro
sync wait없음 (std::future::wait만)blockingWait
fan-in없음collectAll/collectAllRange
fail-fastN/AcollectAll
collect-all-resultsN/AcollectAllTry
first-winsN/AcollectAny
timeoutN/Acoro::timeout
cancellationN/A자동 전파

표준은 단일 코루틴까지만. 합성/대기는 라이브러리.

#코드 리뷰 포인트

  • blockingWait가 라이브러리 함수 안에 있다 → 거의 항상 잘못된 위치.
  • collectAll을 single-thread executor에서 호출 → 직렬화. parallel 의도라면 multi-thread executor 필요.
  • collectAll 후 결과 tuple destructuring을 안 하고 통째로 보유 → 큰 결과는 메모리.
  • timeout과 cancellation의 조합이 누락 → 외부 요청이 영원히 hang 가능.

#자주 보는 안티패턴

// 1. blockingWait 안에서 같은 pool의 Task 기다림
folly::coro::Task<int> Helper(folly::Executor* e) {
return co_await folly::coro::blockingWait(Inner().scheduleOn(e));
// 같은 e면 deadlock
}
// 2. range의 Task를 전부 .scheduleOn(같은 pool) 후 collectAll
std::vector<folly::coro::Task<int>> ts;
for (int i = 0; i < 1000; ++i) ts.push_back(Compute(i).scheduleOn(&pool));
co_await folly::coro::collectAllRange(std::move(ts));
// pool 크기보다 한참 많으면 starvation
// 3. collectAny 사용 후 패자 Task 폐기 - cancellation aware하지 않으면 leak
auto idx = co_await folly::coro::collectAny(slowQuery1, slowQuery2);
// slow 측이 cancel을 무시하면 background에서 계속 실행

#정리

  • blockingWait는 main()과 sync boundary 전용 — 라이브러리 안에서 쓰지 말 것.
  • collectAll / collectAllRange 로 정해진/동적 수의 Task fan-out.
  • collectAllTry로 부분 실패 허용, collectAny로 race.
  • 내부적으로 folly::collectAll Future 합성 재사용 — Futures 인프라 위의 어댑터.
  • timeout/cancellation과 조합해 production-grade 패턴 완성.

#다음 편

Part 15-05: coro::Baton / Mutex에서 코루틴-aware 동기화 원시를 본다.

#관련 항목

Folly Code Review · 68 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 사용의 실전