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

folly::coro::AsyncGenerator — 비동기 스트림

· Hawk · 4분 읽기

한 줄 요약: AsyncGenerator<T>co_yield로 값을 한 번에 하나씩 비동기로 내보내고 소비자는 for co_await로 받는다. backpressure가 자연스럽고 cancellation도 협력적으로 동작한다.

#동기

비동기 스트림을 표현하는 방법은 셋이다.

  1. callbackOnNext, OnComplete, OnError 콜백 3종. RxCpp, Folly Observable 패턴. 가독성과 backpressure가 약점.
  2. push channelChannel<T>::send(...) / recv(). Go의 channel, fibers::Channel. backpressure는 buffer size로 조절.
  3. pull generatorco_yield / for co_await. 소비자가 원할 때만 값을 당겨온다.

AsyncGenerator<T>는 셋째다. pull-based라 자동으로 backpressure가 생긴다. 소비자가 느리면 producer의 co_yield가 suspend 상태로 머문다.

folly::coro::AsyncGenerator<int> Range(int n) {
for (int i = 0; i < n; ++i) {
co_await folly::coro::sleep(std::chrono::milliseconds(10));
co_yield i;
}
}
folly::coro::Task<int> Sum(int n) {
int total = 0;
auto gen = Range(n);
while (auto v = co_await gen.next()) {
total += *v;
}
co_return total;
}

co_yield i는 i를 소비자에게 넘기고 generator는 suspend 한다. 소비자가 다음 next()를 부르면 generator가 resume.

#API

#include <folly/coro/AsyncGenerator.h>
folly::coro::AsyncGenerator<int> Source();
folly::coro::Task<void> Consume() {
auto gen = Source();
// 방법 1: next() — Optional 반환
while (auto opt = co_await gen.next()) {
process(*opt);
}
// 방법 2: for co_await (C++23 range-based)
// CO_FOREACH macro, 또는 직접 풀어쓴 loop
}

next()folly::coro::AsyncGenerator<T>::NextResult를 반환한다. 이것이 bool 변환 가능(값이 있으면 true). end-of-stream이면 비어 있다.

#Reference vs Value yield

// Value generator — co_yield가 복사/이동
folly::coro::AsyncGenerator<std::string> Lines(std::istream& in) {
std::string line;
while (std::getline(in, line)) {
co_yield line;
}
}
// Reference generator — co_yield가 참조 전달, 큰 객체 효율
folly::coro::AsyncGenerator<const LargeMessage&> Messages(Stream& s) {
for (;;) {
auto msg = co_await s.next();
if (!msg) break;
co_yield *msg;
}
}

reference variant는 소비자가 다음 co_await 전까지만 reference가 유효하다는 계약이다. consume-or-copy 패턴.

#for co_await

folly::coro::Task<void> Print(folly::coro::AsyncGenerator<int> gen) {
while (auto v = co_await gen.next()) {
std::cout << *v << "\n";
}
}

표준에 for co_await가 도입되면 이렇게 쓸 수 있다.

folly::coro::Task<void> Print(folly::coro::AsyncGenerator<int> gen) {
CO_FOREACH (int v, gen) { // folly macro
std::cout << v << "\n";
}
}

현재 folly가 제공하는 CO_FOREACH macro가 for co_await를 흉내낸다. 표준이 따라잡으면 사라질 헬퍼.

#내부 구조

// folly/coro/AsyncGenerator.h 약식
template <class Ref, class Value = std::remove_cvref_t<Ref>>
class AsyncGenerator {
public:
class promise_type {
public:
AsyncGenerator get_return_object() noexcept;
std::suspend_always initial_suspend() noexcept; // lazy
auto final_suspend() noexcept; // notify consumer
auto yield_value(Ref v) noexcept; // co_yield
void return_void() noexcept;
void unhandled_exception() noexcept;
private:
Ref* current_ = nullptr;
std::coroutine_handle<> consumer_;
folly::exception_wrapper exception_;
};
class NextAwaitable {
auto await_suspend(std::coroutine_handle<> h) noexcept;
auto await_resume();
};
NextAwaitable next() noexcept;
};

핵심은 두 코루틴(producer/consumer)이 서로 resume하는 패턴이다.

  1. consumer가 co_await gen.next() 호출.
  2. await_suspend가 producer handle을 반환 → symmetric transfer로 producer resume.
  3. producer가 co_yield v → 값을 current_에 저장, consumer handle 반환 → consumer resume.
  4. 반복.

end-of-stream은 producer가 return_void에 도달하면 final_suspend에서 consumer를 resume + 빈 NextResult.

#합성 — chain / transform

template <class T, class F>
folly::coro::AsyncGenerator<std::invoke_result_t<F, T>>
transform(folly::coro::AsyncGenerator<T> src, F f) {
while (auto v = co_await src.next()) {
co_yield f(std::move(*v));
}
}
folly::coro::Task<void> Pipeline() {
auto evens = transform(Range(10), [](int x) { return x * 2; });
while (auto v = co_await evens.next()) {
std::cout << *v << "\n";
}
}

함수형 stream 조합이 일반 함수 호출로 표현된다. RxCpp의 operator 카탈로그 같은 별도 DSL이 필요 없다.

#Cancellation 전파

folly::coro::Task<void> ConsumeWithTimeout(
folly::coro::AsyncGenerator<int> gen) {
auto src = folly::CancellationSource{};
auto token = src.getToken();
auto consumeTask = [&]() -> folly::coro::Task<void> {
while (auto v = co_await gen.next()) {
process(*v);
}
}();
// 5초 뒤 cancel
std::thread([s = src]() mutable {
std::this_thread::sleep_for(std::chrono::seconds(5));
s.requestCancellation();
}).detach();
co_await folly::coro::co_withCancellation(token, std::move(consumeTask));
}

cancellation은 producer 코루틴의 co_await suspend point에서 trigger된다. producer가 awaitable이 cancel-aware라면 OperationCancelled 예외로 깨어난다.

#Push channel과의 비교

항목AsyncGeneratorChannel
방향pull (consumer가 당김)push (producer가 보냄)
backpressure자동 (suspend)buffer size로 조절
multi-consumer안 됨 (single owner)가능
multi-producer안 됨가능
합성함수 합성별도 connect/wire

단일 producer ↔ 단일 consumer 모델이라면 AsyncGenerator가 단순. 팬아웃/팬인이 필요하면 Channel.

#std와의 비교

항목std::generator (C++23)folly::coro::AsyncGenerator
동기/비동기sync onlyasync (suspend 가능)
co_await안 됨yield 사이에 가능
reference yield지원지원
for-rangefor (auto v : gen)CO_FOREACH 또는 next()
표준화C++23비표준

std::generator가 sync only라는 점이 결정적 차이. 비동기 stream은 표준에 아직 자리가 없다.

#코드 리뷰 포인트

  • generator를 두 번 iterate 시도 — single-shot이라 두 번째는 즉시 종료.
  • reference yield인데 소비자가 다음 next() 후에 reference를 잡고 있음 — UAF.
  • producer 코루틴이 cancel-aware하지 않은 외부 작업(blocking I/O)을 await — cancellation이 안 통함.
  • generator를 lifetime이 짧은 stack-allocated 객체로 보유 → 코루틴 frame이 살아있는 동안 generator도 살아야 함.

#자주 보는 안티패턴

// 1. reference yield 결과를 collection에 보관
std::vector<const LargeMessage*> all;
while (auto v = co_await gen.next()) {
all.push_back(&*v); // 다음 iter에서 dangling
}
// 2. generator를 다중 consumer에게 공유
auto gen = Source();
std::thread t1([&] { folly::coro::blockingWait(consume(gen)); });
std::thread t2([&] { folly::coro::blockingWait(consume(gen)); }); // race
// 3. eager 평가 시도
auto all = collectAll(gen); // AsyncGenerator는 collect 직접 안 됨

#정리

  • AsyncGenerator<T>는 pull-based 비동기 stream이다.
  • co_yield로 값을 내보내고 co_await gen.next()로 받는다.
  • producer/consumer는 symmetric transfer로 서로 resume.
  • reference yield로 큰 객체를 복사 없이 전달 가능, 단 consume-or-copy 계약.
  • std::generator(sync)와는 다른 도메인 — async stream은 표준에 아직 없다.

#다음 편

Part 15-04: blockingWait / collectAll에서 sync 경계와 fan-in을 본다.

#관련 항목

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