folly::CPUThreadPoolExecutor — CPU-bound 작업의 표준 thread pool
한 줄 요약:
CPUThreadPoolExecutor는 fixed-size thread pool로 CPU-bound 작업을 실행한다. priority/queue/factory가 모두 교체 가능한 라이브러리급 구현이다.
#동기 — 표준에 thread pool이 없다
C++ 표준에는 thread pool이 없다. std::async(std::launch::async)가 매번 새 thread를 만들 수도 안 만들 수도 있다는 점이 모호하고, std::thread는 직접 manage해야 한다.
Folly는 production thread pool을 정확히 두 모양으로 제공한다.
CPUThreadPoolExecutor— CPU 집약 작업 (parse, compute, serialize)IOThreadPoolExecutor— I/O 작업 (libevent EventBase 기반)
두 pool의 분리는 blocking 작업이 CPU 작업을 starve하지 못하게 한다.
#기본 사용
#include <folly/executors/CPUThreadPoolExecutor.h>
folly::CPUThreadPoolExecutor pool(8); // 8 threads
pool.add([] { doExpensiveWork();});
// Future와 통합computeSemi() .via(&pool) .thenValue([](int x) { return process(x); });constructor의 첫 인자는 thread 개수다. 일반적으로 std::thread::hardware_concurrency() 또는 그 절반/배수를 쓴다.
#구조
// folly/executors/CPUThreadPoolExecutor.h (요약)class CPUThreadPoolExecutor : public ThreadPoolExecutor { public: CPUThreadPoolExecutor( size_t numThreads, std::unique_ptr<BlockingQueue<CPUTask>> taskQueue = std::make_unique<UnboundedBlockingQueue<CPUTask>>(), std::shared_ptr<ThreadFactory> threadFactory = std::make_shared<NamedThreadFactory>("CPUThreadPool"));
void add(Func) override; void add(Func, std::chrono::milliseconds timeout, Func expireCallback); void addWithPriority(Func, int8_t priority) override;
size_t numActiveThreads() const; size_t numPendingTasks() const;};세 부분이 조합 가능하다.
- BlockingQueue — 어떤 큐 정책 (unbounded / bounded / priority)
- ThreadFactory — thread 생성/이름/affinity
- CPUTask — task 자체
#모델 위치
CPUThreadPoolExecutor는 Fixed thread pool 모델 — 공유 큐 + N 워커.
평행성을 얻고 스레드 수를 bounded로 유지하는 균형점이다. 단일 큐가 hot이면 work-stealing(또는 priority queue로 분리)으로 contention을 분산할 수 있다.
#Queue 선택
// 무제한 — 메모리 폭발 가능auto q1 = std::make_unique<folly::UnboundedBlockingQueue<CPUTask>>();
// 제한 — 가득 차면 add()가 blockauto q2 = std::make_unique<folly::LifoSemMPMCQueue<CPUTask>>(1000);
// priority queueauto q3 = std::make_unique<folly::PriorityLifoSemMPMCQueue<CPUTask>>( 3, // 3 priority levels 1000); // capacity per level
folly::CPUThreadPoolExecutor pool(8, std::move(q3));
pool.addWithPriority([] { ... }, folly::Executor::HI_PRI);pool.addWithPriority([] { ... }, folly::Executor::LO_PRI);priority queue는 latency-sensitive task와 background batch를 분리할 때 유용하다.
#ThreadFactory
auto factory = std::make_shared<folly::NamedThreadFactory>("Parser");folly::CPUThreadPoolExecutor pool(4, factory);
// 생성된 thread는 "Parser-0", "Parser-1", ... 이름을 가짐top -H -p <pid> 또는 perf trace에서 thread 이름이 보이면 디버깅이 한층 쉬워진다. 모든 thread pool에 명시적 이름을 주는 게 권장이다.
// 사용자 정의 factory — CPU affinityclass AffinityThreadFactory : public folly::ThreadFactory { public: std::thread newThread(folly::Func&& f) override { return std::thread([f = std::move(f)]() mutable { cpu_set_t set; CPU_ZERO(&set); CPU_SET(specificCore_, &set); pthread_setaffinity_np(pthread_self(), sizeof(set), &set); f(); }); }};#Task — work + metadata
struct CPUTask { Func func; std::chrono::steady_clock::time_point enqueueTime; std::chrono::milliseconds expireTime{0}; Func expireCallback; int8_t priority{Executor::MID_PRI};};enqueueTime은 queue에서 대기한 시간을 측정한다. tail latency 분석에 핵심이다.
pool.add( [] { doWork(); }, // task std::chrono::milliseconds(100), // 100ms 내에 시작 안 되면 expire [] { LOG(WARN) << "task expired"; }); // expireCallback#stop / join 정책
class ThreadPoolExecutor { public: void stop(); // 즉시 신규 task 거절, in-flight task는 진행 void join(); // 모든 in-flight 끝날 때까지 wait void setNumThreads(size_t); // dynamic resize};
// destructor가 자동 join{ folly::CPUThreadPoolExecutor pool(8); pool.add(...);} // ← 여기서 join (in-flight task 끝날 때까지)destructor가 모든 task가 끝날 때까지 block한다. 이를 모르면 원치 않게 block되는 경우가 있다.
#std::thread / std::async와 비교
// std::thread — 직접 managestd::vector<std::thread> threads;for (int i = 0; i < N; ++i) { threads.emplace_back([] { work(); });}for (auto& t : threads) t.join();
// std::async — launch policy 모호auto f = std::async(std::launch::async, [] { return work(); });int v = f.get(); // thread 생성/파괴 비용
// folly::CPUThreadPoolExecutor — pool reusestatic folly::CPUThreadPoolExecutor pool(8);pool.add([] { work(); }); // thread 재사용std::async는 매번 thread를 만들 수 있다(implementation defined). pool은 fixed thread를 재사용한다.
#InlineExecutor와의 차이
| 항목 | InlineExecutor | CPUThreadPoolExecutor |
|---|---|---|
| 실행 | caller thread | pool worker thread |
| 비동기성 | 없음 | 있음 |
| 메모리 | 0 | thread당 stack + queue |
| 사용 사례 | test, 단축 | production |
#코드 리뷰 포인트
- thread 수가 hardcoded인가?
std::thread::hardware_concurrency()또는 config로 받자. - queue가 unbounded인가? burst 트래픽에서 메모리 폭발 가능. bounded + expire 정책 고려.
- thread 이름이 적절한가? “Pool-0”보다 “Parser-0”, “Encoder-0”이 디버깅에 좋다.
- 여러 pool이 있는가? I/O와 CPU 분리, latency-sensitive와 batch 분리.
#자주 보는 안티패턴
// 1. 매 함수 호출마다 새 pool 생성void handle() { folly::CPUThreadPoolExecutor pool(4); // thread 생성/파괴 — 매우 비쌈 pool.add([] { work(); });} // destructor가 join
// 2. unbounded queue + slow consumerfolly::CPUThreadPoolExecutor pool(2); // 2 threadfor (int i = 0; i < 1'000'000; ++i) { pool.add([] { sleep(1); }); // 큐가 무한정 자람}
// 3. pool 안에서 .get() 호출pool.add([&] { auto v = anotherSemi().via(&pool).get(); // deadlock 위험 // pool worker 가 자기 자신을 wait});
// 4. CPU pool에서 blocking I/Opool.add([] { read(fd, buf, n); // CPU thread 가 I/O로 block — pool starve});(3)은 pool 크기가 작을 때 모든 worker가 서로를 wait해 deadlock한다. CPU pool에서 같은 CPU pool에 schedule된 결과를 wait하지 마라.
#정리
CPUThreadPoolExecutor는 fixed-size pool로 CPU 집약 작업을 실행한다.- Queue/ThreadFactory/Task가 모두 교체 가능한 모듈식 구조다.
- priority queue로 latency-sensitive와 batch를 분리한다.
- thread 이름을 명시해 디버깅 가능성을 높인다.
- destructor가 join하므로 원치 않는 block에 주의한다.
- I/O 작업은 별도의
IOThreadPoolExecutor로 분리한다.
#다음 편
Part 3-03: IOThreadPoolExecutor에서 libevent 기반 I/O pool을 본다.
#관련 항목
Folly Code Review · 15 of 89
- 1 Folly Code Review — Meta의 production-grade C++ 라이브러리 코드 분석
- 2 Folly 개요 — Meta가 production에서 검증한 utility 모음 분석
- 3 Folly vs Abseil 철학 비교 — performance-first vs std-compatible
- 4 Folly 빌드와 fbcode 환경 — monorepo의 그림자
- 5 Folly API stability 정책 — 어떤 보장도 없다는 솔직함
- 6 Folly production validation 문화 — peta-scale에서 단련된 코드
- 7 folly::Future 분석 — std::future의 한계를 넘는 composable async
- 8 folly::Promise·makeFuture — Future를 만드는 두 길
- 9 folly::SemiFuture vs Future — executor binding의 명시화
- 10 folly::Future thenValue·thenError·thenTry — continuation 체인 분석
- 11 folly::collect·collectAll·collectAny — fan-in 패턴 분석
- 12 folly::Future retry·window·via — 제어 흐름 조합자
- 13 folly::fibers 분석 — M:N stackful coroutine
- 14 folly::InlineExecutor — 호출자 thread에서 즉시 실행
- 15 folly::CPUThreadPoolExecutor — CPU-bound 작업의 표준 thread pool
- 16 folly::IOThreadPoolExecutor — libevent 기반 I/O pool
- 17 folly::ManualExecutor — 결정적 테스트를 위한 수동 진행
- 18 folly::EventBase 분석 — libevent 이벤트 루프의 핵심
- 19 folly::IOBuf 분석 — zero-copy buffer chain의 기본 단위
- 20 folly::IOBufQueue — chain의 push/pull 추상화
- 21 folly::io::Cursor·RWCursor — chain 위의 stream
- 22 folly Zero-copy 패턴 — IOBuf로 ScatterGather I/O 표현
- 23 folly::IOBuf shared semantics — clone·unshare·takeOwnership
- 24 folly::FBString 분석 — SSO + COW 구현
- 25 folly의 fmt::format 통합 — 모던 포맷팅 채택
- 26 folly::StringPiece — string_view 호환 분석
- 27 folly Join·Split utilities — 문자열 분해와 결합
- 28 folly::to·tryTo — text↔num 변환 분석
- 29 folly Conv Customization — 사용자 타입 지원
- 30 folly Conv 성능 비교 — sprintf·stringstream 대비
- 31 folly::F14ValueMap vs std::unordered_map
- 32 folly::F14NodeMap — stable pointer가 필요할 때
- 33 folly::F14VectorMap — cache-friendly iteration
- 34 folly::F14FastMap — auto-select 동작
- 35 folly F14 internals — SIMD probing 메커니즘
- 36 folly::small_vector — inline storage 분석
- 37 folly::FixedString — compile-time string
- 38 folly::AtomicHashMap — lock-free read 분석
- 39 folly::ConcurrentHashMap — sharded 동시 해시 맵
- 40 folly::EvictingCacheMap — LRU 구현 분석
- 41 folly::Synchronized — lock wrapper 패턴
- 42 folly::SharedMutex 분석
- 43 folly::Baton — one-shot wait 동기화
- 44 folly::RWSpinLock 분석
- 45 folly::PicoSpinLock — 1-byte spinlock
- 46 folly::ProducerConsumerQueue — SPSC 큐 분석
- 47 folly::MPMCQueue — multi-producer multi-consumer
- 48 folly::UnboundedQueue — 동적 크기 lock-free
- 49 folly::fibers::Channel — Go-like channel
- 50 folly::dynamic — JSON-like dynamic type 분석
- 51 folly JSON conversion — toJson·parseJson
- 52 folly dynamic ↔ struct — manual marshaling
- 53 folly dynamic Visitor pattern — type별 분기
- 54 folly::Singleton vs Meyers/static — 왜 Folly의 Singleton인가
- 55 folly::SingletonVault 분석 — 등록·소멸·의존성
- 56 folly::Singleton try_get·try_get_fast — TLS-cached 접근
- 57 folly::ExceptionWrapper — type-erased exception holder
- 58 folly::ScopeGuard·SCOPE_EXIT — RAII cleanup
- 59 folly::Optional vs std::optional
- 60 folly::Function vs std::function
- 61 folly::Lazy — 지연 초기화 wrapper
- 62 folly Meta 스타일 code review 패턴
- 63 folly anti-patterns — 잘못 쓰면 std보다 느림
- 64 folly vs std 선택 기준 분석
- 65 folly::coro 개요 — production C++20 코루틴 어댑터
- 66 folly::coro::Task — lazy single-shot 코루틴
- 67 folly::coro::AsyncGenerator — 비동기 스트림
- 68 folly coro blockingWait·collectAll — 동기 경계와 fan-in
- 69 folly::coro::Baton·Mutex — 코루틴-aware 동기화
- 70 folly::Expected — 결과 또는 오류
- 71 folly::Try — Future 결과 wrapper
- 72 folly::Try vs Expected 선택 기준
- 73 folly::Range — 일반 iterator pair
- 74 folly::Uri — URL 파서
- 75 folly Fingerprint64·128 — 분산 hash
- 76 folly SpookyHashV2 — fast non-crypto hash
- 77 folly::Init — main() 부트스트랩
- 78 folly::Indestructible — global lifetime 패턴
- 79 folly::MicroLock — 1-byte 락
- 80 folly::MicroSpinLock — 가장 좁은 spin lock
- 81 folly::format — legacy formatter 분석
- 82 folly::demangle — typeid 디망글링
- 83 folly::DynamicConverter — dynamic ↔ struct
- 84 folly::RecordIO — append-only 로그 파일 포맷
- 85 folly::io::Compression — zstd·lz4·snappy wrapper
- 86 folly::AsyncIO — io_uring·Linux AIO
- 87 folly::CancellationToken — 코루틴·Future 취소 전파
- 88 folly::observer — hot config의 atomic refresh
- 89 fbcode 패턴 모음 — folly 사용의 실전
관련 글
folly::EventBase 분석 — libevent 이벤트 루프의 핵심
EventBase는 libevent의 event_base를 wrap한 단일 thread event loop. file descriptor, timer, cross-thread message를 한 번에 처리한다.
같은 시리즈에서 이어 읽기
folly::ManualExecutor — 결정적 테스트를 위한 수동 진행
ManualExecutor는 schedule된 task를 자동 실행하지 않고 run()/drive() 호출 시점에만 진행한다. 비동기 코드의 단위 테스트에 결정성을 부여한다.
같은 시리즈에서 이어 읽기
folly::IOThreadPoolExecutor — libevent 기반 I/O pool
IOThreadPoolExecutor는 각 worker thread에 EventBase를 두어 libevent 기반 I/O와 timer를 처리한다.
같은 시리즈에서 이어 읽기
이 글을 참조하는 글 (5)
- folly::MPMCQueue — multi-producer multi-consumer — Folly Code Review
- folly::IOThreadPoolExecutor — libevent 기반 I/O pool — Folly Code Review
- folly::InlineExecutor — 호출자 thread에서 즉시 실행 — Folly Code Review
- folly::Future retry·window·via — 제어 흐름 조합자 — Folly Code Review
- folly::collect·collectAll·collectAny — fan-in 패턴 분석 — Folly Code Review