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

folly::coro::Baton·Mutex — 코루틴-aware 동기화

· Hawk · 4분 읽기

한 줄 요약: std::mutex는 thread를 block한다. 코루틴 안에서 그러면 executor의 thread 한 칸을 통째로 잠근다. folly::coro::Mutex/Baton코루틴만 suspend해 같은 thread가 다른 일을 한다.

#동기

코루틴의 핵심 자원은 thread가 아니라 frame이다. thread는 executor pool에 소수만 있다(보통 CPU 코어 수). 코루틴 frame은 수십만 개 살 수 있다. 그런데 코루틴 안에서 std::mutex::lock()을 쓰면 thread가 block된다. frame은 살아있는데 thread가 없는 상황.

// Bad
folly::coro::Task<void> Process(SharedData& d) {
std::lock_guard lk(d.mu); // ← thread block. pool 1/n 잠금.
co_await asyncWrite(d.snapshot());
d.update();
}

이 코드는 co_await 동안 thread가 자유로워지긴 한다. 그러나 lock_guard가 살아있는 동안 mutex를 점유하므로 다른 코루틴이 lock()을 시도하면 thread가 block된다. executor 4-thread pool에서 4개 코루틴이 lock 경합하면 모든 thread가 idle하지 못한다.

coro::Mutex는 lock 경합 시 코루틴만 suspend, thread는 다른 코루틴을 실행한다.

#coro::Baton

#include <folly/coro/Baton.h>
folly::coro::Baton baton;
folly::coro::Task<void> Waiter() {
co_await baton; // suspend until post
// do work
}
folly::coro::Task<void> Poster() {
// ... compute ...
baton.post(); // wake all waiters
}

Baton은 1-shot edge-triggered notification. post()가 한 번 호출되면 그 이후의 co_await은 즉시 통과. reset()으로 재사용 가능.

multi-waiter 가능:

folly::coro::Baton ready;
auto t1 = []() -> folly::coro::Task<void> { co_await ready; /* ... */ }();
auto t2 = []() -> folly::coro::Task<void> { co_await ready; /* ... */ }();
auto t3 = []() -> folly::coro::Task<void> { co_await ready; /* ... */ }();
// ... 시간이 흐른 후
ready.post(); // 셋 다 동시에 깨어남

여러 코루틴이 같은 baton을 기다릴 수 있다. post() 시 모두 깨운다.

#coro::Mutex

#include <folly/coro/Mutex.h>
folly::coro::Mutex mu;
int counter = 0;
folly::coro::Task<void> Increment() {
auto lock = co_await mu.co_scoped_lock();
// critical section — 다른 코루틴이 이 mu에 락 시도하면 suspend
co_await asyncWork();
++counter;
// lock 소멸자가 unlock
}

co_scoped_lock()은 awaitable. await 결과로 RAII lock guard 반환. critical section에서 다른 co_await도 가능하다 — await 도중에도 lock은 유지된다.

이게 std::mutex와 결정적으로 다른 점.

// std::mutex와 std::async — lock 들고 async 안 됨
std::mutex m;
auto fut = std::async(std::launch::async, [&] {
std::lock_guard lk(m);
return otherAsync().get(); // block — thread 다시 잠김
});

coro::Mutex는 코루틴 frame에 lock 상태가 매여 있어 suspend 동안 thread가 다른 코루틴을 돌릴 수 있다.

#coro::SharedMutex

folly::coro::SharedMutex mu;
Config cfg;
folly::coro::Task<Config> Read() {
auto lk = co_await mu.co_scoped_lock_shared();
co_return cfg;
}
folly::coro::Task<void> Write(Config c) {
auto lk = co_await mu.co_scoped_lock();
cfg = std::move(c);
}

reader/writer 분리. read가 많은 hot path에서 유리.

#coro::Semaphore

folly::coro::Semaphore sem{4}; // 최대 4 동시
folly::coro::Task<void> BatchProcess(std::vector<Item> items) {
std::vector<folly::coro::Task<void>> tasks;
for (auto& item : items) {
tasks.push_back([&, item]() -> folly::coro::Task<void> {
auto guard = co_await sem.co_scoped_lock();
co_await Process(item);
}());
}
co_await folly::coro::collectAllRange(std::move(tasks));
}

동시 처리 수를 제한. backpressure pattern. 외부 API rate limit, DB connection pool 같은 자리에 자연스럽다.

#내부 구조 — wait list

// folly/coro/Mutex.h 약식
class Mutex {
public:
auto co_lock() noexcept {
struct Awaiter {
Mutex* mu;
std::coroutine_handle<> handle_;
Awaiter* next_ = nullptr;
bool await_ready() noexcept {
return mu->tryLock();
}
void await_suspend(std::coroutine_handle<> h) noexcept {
handle_ = h;
mu->addWaiter(this); // atomic linked list 앞에 push
}
void await_resume() noexcept {}
};
return Awaiter{this};
}
void unlock() noexcept {
auto* w = popWaiter(); // atomic pop
if (w) {
// resume on executor — current thread 또는 captured executor
executor_->add([h = w->handle_] { h.resume(); });
} else {
locked_.store(false);
}
}
};

waiter들이 atomic linked list에 줄을 선다. unlock()이 한 명을 깨워 그의 코루틴을 executor에 schedule한다. 곧바로 그 자리에서 resume하면 stack이 깊어지므로 executor를 거치는 게 정석.

#std::mutex와의 비교

항목std::mutexcoro::Mutex
대기 시thread block코루틴 suspend
재진입non-recursivenon-recursive
조건 변수std::condition_variable 별도Baton/Channel 별도
critical section 안 asyncblock 가능 (위험)자연스럽게 OK
sizeof~40 byte (glibc)보통 더 작음 (waiter list head만)

std::mutex를 코루틴 안에서 짧은 동기 critical section에만 쓰는 건 OK. 그러나 critical section 안에서 co_await이 등장하면 무조건 coro::Mutex.

#fiber와의 관계

folly::fibers::Baton이 fibers 도메인의 짝이다.

도메인동기화어디서 suspend
std threadstd::mutex / condition_variableOS scheduler
folly::fibersfibers::Baton / fibers::TimedMutexfiber scheduler
folly::corocoro::Mutex / coro::Batonexecutor

세 시스템이 비슷한 API로 추상화를 통일하려는 흐름이 보인다.

#코드 리뷰 포인트

  • 코루틴 안 critical section에 co_await이 있는데 std::mutex 사용 → 즉시 coro::Mutex로 교체 대상.
  • Baton을 multi-shot으로 쓰려고 함 → reset()이 필요하거나 Channel 고려.
  • semaphore 슬롯이 lock guard 없이 acquire/release 분리 → 예외 시 leak. co_scoped_lock 사용.
  • mutex가 hot path에 있고 read가 압도적이면 SharedMutex 검토.

#자주 보는 안티패턴

// 1. coro::Mutex 사용하지만 std::lock_guard와 혼용
folly::coro::Mutex mu;
{
std::lock_guard lk(mu); // 컴파일 에러 또는 wrong overload
co_await ...;
}
// 2. Baton을 broadcast로 사용한 뒤 reset 잊음
baton.post();
// 다음 라운드 시작
co_await baton; // 즉시 통과 — 의도와 다름
// 3. Semaphore acquire 후 await 도중 throw, release 안 됨
co_await sem.acquire(); // raw acquire
co_await mayThrow(); // 예외 → semaphore 영원히 점유
co_await sem.release();
// → co_scoped_lock으로 RAII 처리해야

#정리

  • 코루틴 안 동기화는 thread를 block하지 않는 코루틴-aware primitive가 필요하다.
  • coro::Baton은 1-shot notification, multi-waiter 가능.
  • coro::Mutex/SharedMutex/Semaphore는 std와 표층 API가 비슷하지만 suspend 의미론.
  • critical section 안에서 co_await이 등장한다면 무조건 coro variant.
  • waiter는 atomic linked list로 줄을 서고 unlock 시 executor를 거쳐 resume.

#다음 편

Part 16에서 Expected/Try — error 표현의 두 갈래를 다룬다.

#관련 항목

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