Coroutines are the most transformative C++ feature since move semantics. With C++20, the language gained native coroutine support through the co_await, co_yield, and co_return keywords — but the standard library did not provide any concrete coroutine types. This intentional gap left the ecosystem to innovate, and three libraries have emerged as the leading options: cppcoro by Lewis Baker, Boost.Cobalt from the Boost organization, and concurrencpp by David Haim.
This article compares these libraries across their programming models, performance characteristics, ecosystem integration, and suitability for production C++ services. We focus on practical concerns: how to write, debug, and deploy coroutine-based C++ applications.
Understanding C++20 Coroutines
C++20 coroutines are stackless — they don’t have their own stack, unlike Go goroutines or Rust async tasks. When a coroutine suspends at co_await, it stores only the local variables that are live across the suspension point, and the coroutine frame is allocated on the heap (or via a custom allocator). This makes them extremely efficient but also places the burden of lifetime management on the library.
The C++20 standard defines the coroutine machinery — promise_type, awaitable, awaiter — but leaves the actual task types, schedulers, and I/O integration to libraries. This is where cppcoro, Boost.Cobalt, and concurrencpp differ fundamentally.
Library Comparison
| Feature | cppcoro | Boost.Cobalt | concurrencpp |
|---|---|---|---|
| Stars | 3,858 | 342 | 2,756 |
| Latest Update | Jan 2024 | June 2026 | May 2025 |
| Core Abstraction | task<T>, generator<T> | cobalt::task<T>, cobalt::generator<T> | concurrencpp::result<T> |
| I/O Integration | io_service + socket wrappers | ASIO integration (native) | concurrencpp::timer, custom executors |
| Executor Model | Single io_service | ASIO io_context | Thread pool + manual executor |
| Thread Safety | Single-threaded I/O loop | ASIO strand model | Thread-safe executors |
| Sync Primitives | async_mutex, async_manual_reset_event | cobalt::channel, cobalt::mutex | concurrencpp::timer_queue |
| Cancellation | Via cancellation_token | ASIO cancellation slots | concurrencpp::timer::cancel() |
| Debugging Support | Limited | ASIO handler tracking | Stack trace on exception |
cppcoro: The Pioneer
cppcoro, created by Lewis Baker (a key contributor to the C++ coroutine TS), was the first comprehensive coroutine library. It established many patterns that later libraries adopted, including the task<T> type and generator<T> for synchronous coroutines.
Key strengths:
- Battle-tested design: The
task<T>pattern from cppcoro influenced both Boost.Cobalt and the proposedstd::execution - File I/O operations:
cppcoro::read_file()andcppcoro::write_file()for async file handling - Network wrappers:
cppcoro::net::socketwrappers that integrate with theio_service - Generator support:
cppcoro::generator<T>for lazy range generation usingco_yield
Limitations:
- No longer actively maintained (last update January 2024)
- Limited to a single
io_service— no multi-threaded execution - No support for ASIO’s
io_contextnatively (uses its ownio_service)
| |
Boost.Cobalt: The ASIO-Native Solution
Boost.Cobalt (formerly Boost.Async) is the Boost organization’s answer to C++20 coroutines. It is built as a thin layer on top of Boost.ASIO, leveraging ASIO’s battle-tested I/O infrastructure, executor model, and cancellation support.
Key strengths:
- ASIO integration: Every
cobalt::task<T>runs on an ASIOio_context, enabling seamless integration with network I/O, timers, and signals - Channel-based communication:
cobalt::channel<T>for coroutine-to-coroutine message passing (like Go channels) - Active maintenance: Part of Boost, with regular releases and LTS support
- Cancellation: Native ASIO cancellation slots propagate through coroutine chains
co_mainentry point: Simplifies application startup withcobalt::main
| |
concurrencpp: The Executor-First Approach
concurrencpp takes a fundamentally different approach — instead of being tied to a single I/O event loop, it provides a hierarchy of executors (thread pool, manual, inline, worker thread) that coroutines run on.
Key strengths:
- Flexible executor model: Run coroutines on thread pools, single threads, or custom executors
concurrencpp::when_all+when_any: Powerful composition primitivesconcurrencpp::timer: Schedule coroutine execution after a delay or at regular intervals- Exception propagation: Exceptions thrown in coroutines are captured and re-thrown when the result is consumed
- Runtime polymorphism:
concurrencpp::runtimemanages executor lifecycles
| |
Docker Dev Environment for Coroutine Testing
| |
Choosing the Right Coroutine Library
| Use Case | Recommended Library | Rationale |
|---|---|---|
| Greenfield ASIO project | Boost.Cobalt | Native ASIO integration, maintained, Boost ecosystem |
| Learning coroutines | cppcoro | Simple API, excellent examples, established patterns |
| CPU-bound parallel computation | concurrencpp | Thread pool executors, when_all for fan-out |
| High-throughput network service | Boost.Cobalt | ASIO’s proven I/O performance, cancellation support |
| Mixed I/O + CPU workloads | concurrencpp | Separate executors for I/O and compute |
| Legacy code modernization | cppcoro | Easier incremental adoption, sync_wait for bridging |
FAQ
Are C++20 coroutines as fast as hand-written state machines?
In most cases, yes. C++20 coroutines are implemented as compiler-generated state machines, and modern compilers (GCC 12+, Clang 15+) optimize coroutine frames aggressively. In benchmarks, coroutine-based async code typically achieves 90-98% of the throughput of hand-rolled callback-based state machines, with significantly less code and fewer bugs.
How much heap memory does a coroutine frame consume?
A typical coroutine frame is 64-256 bytes, allocated once per coroutine invocation. Libraries like cppcoro and Boost.Cobalt support custom allocators (via operator new in the promise_type) to use arena or pool allocators, eliminating per-coroutine heap overhead in hot paths.
Can I mix coroutine libraries in the same codebase?
Technically yes, but practically it creates maintenance headaches. Each library has its own task<T> type with different semantics — you cannot co_await a cobalt::task from a concurrencpp::result. Choose one library as your primary coroutine framework and use it consistently. If you must bridge libraries, use sync_wait-style primitives at the boundary, but accept the performance cost.
Does Boost.Cobalt require the full Boost distribution?
Boost.Cobalt depends on Boost.ASIO, Boost.System, and Boost.Container. With Boost’s BCP tool, you can extract just these modules (about 3 MB of headers). Alternatively, use a package manager like Conan or vcpkg which installs Boost in a modular fashion.
How do I debug a suspended coroutine?
GDB 12+ and LLDB 16+ include experimental coroutine frame inspection. You can inspect the promise_type and local variables stored in the coroutine frame. With Boost.Cobalt, enable ASIO handler tracking (-DBOOST_ASIO_ENABLE_HANDLER_TRACKING) for detailed logs of which coroutines are suspended and where. concurrencpp provides automatic stack trace capture on exceptions.
💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com
For more C++ concurrency topics, see our C-level coroutine libraries comparison and async I/O runtime libraries guide. For task parallelism patterns, check our taskflow and thread pool comparison.