Coroutine Libraries: cppcoro, Asio, libunifex
C++20 defines the coroutine framework — the machinery of promise types, handles, and awaitables — but ships no concrete coroutine types. Building your own task<T> or generator<T> from scratch, as shown in the previous lessons, is instructive but impractical for production work. This lesson surveys the three most important library-level coroutine ecosystems: cppcoro (a reference implementation of higher-level coroutine primitives), Boost.Asio (the dominant C++ async I/O library, now with first-class coroutine support), and libunifex (Meta's exploration of the senders/receivers model that influenced C++23).
cppcoro — the reference coroutine library
Lewis Baker's cppcoro library (github.com/lewissbaker/cppcoro) is the canonical reference implementation of C++20 coroutine primitives. It was developed alongside the coroutine standardisation effort and provides production-quality implementations of exactly the types the standard chose not to include: task<T>, generator<T>, async_generator<T>, synchronisation primitives for coroutines, and an I/O scheduler. If you are building async code today and do not want to write your own promise types, cppcoro gives you a complete toolkit.
task<T> — lazy async coroutine
cppcoro::task<T> is a coroutine type that starts suspended and runs only when co_awaited. This laziness prevents unnecessary work: if you create a task but never await it, the coroutine body never executes. The result is returned through the promise and is available via the awaiter's await_resume().
#include <cppcoro/task.hpp>
#include <cppcoro/sync_wait.hpp>
#include <print>
cppcoro::task<int> get_answer()
{
co_return 42;
}
cppcoro::task<> print_answer()
{
auto t = co_await get_answer(); // chains tasks
std::println("the answer is {}", t);
}
cppcoro::task<> demo()
{
int a = co_await get_answer(); // suspends, resumes with 42
std::println("answer = {}", a);
co_await print_answer(); // suspends, resumes when done
}
int main()
{
// sync_wait blocks the calling thread until the coroutine chain completes
cppcoro::sync_wait(demo());
}when_all — concurrent fan-out
Chaining coroutines sequentially is straightforward. Running multiple coroutines concurrently and waiting for all to finish requires an explicit mechanism. cppcoro::when_all takes a collection of tasks and returns a task that completes when all of them have finished, gathering their results into a tuple. This is the coroutine equivalent of std::async with multiple futures.
#include <cppcoro/when_all.hpp>
cppcoro::task<int> fetch_user_count() { co_return 1000; }
cppcoro::task<int> fetch_item_count() { co_return 250; }
cppcoro::task<> fetch_all()
{
// Both tasks run concurrently; results collected in a tuple
auto [users, items] = co_await cppcoro::when_all(
fetch_user_count(),
fetch_item_count()
);
std::println("users: {}, items: {}", users, items);
}
int main()
{
cppcoro::sync_wait(fetch_all());
}Generator types: sync, async, and recursive
cppcoro provides three generator variants, each addressing a different need. The synchronous generator<T>mirrors what we built manually and works identically to our implementation. The async_generator<T> adds co_await support inside the generator body — useful for generators that fetch data from a network or database between yields. The recursive_generator<T>allows a generator to yield the elements of another generator as a flat sequence, enabling efficient tree traversal without O(depth) copies.
#include <cppcoro/generator.hpp>
#include <cppcoro/async_generator.hpp>
#include <cppcoro/recursive_generator.hpp>
// Synchronous generator — drop-in for std::generator
cppcoro::generator<int> iota(int start = 0, int step = 1) noexcept
{
auto value = start;
for (int i = 0;; ++i) {
co_yield value;
value += step;
}
}
// Async generator — can co_await between yields
cppcoro::async_generator<std::string> read_lines(File file)
{
while (!file.eof()) {
co_yield co_await file.read_line(); // I/O between yields
}
}
// Recursive generator — flatten a tree without copying
struct Node { int value; std::vector<Node> children; };
cppcoro::recursive_generator<int> traverse(const Node& n)
{
co_yield n.value;
for (const auto& child : n.children)
co_yield cppcoro::elements_of(traverse(child)); // flatten child
}
// Usage
int main()
{
for (int x : iota(0, 2) | std::views::take(5))
std::print("{} ", x); // 0 2 4 6 8
}| cppcoro type | Header | Use for |
|---|---|---|
| task<T> | <cppcoro/task.hpp> | Lazy async coroutine returning a single value |
| generator<T> | <cppcoro/generator.hpp> | Synchronous lazy sequence |
| async_generator<T> | <cppcoro/async_generator.hpp> | Sequence where each element may require I/O |
| recursive_generator<T> | <cppcoro/recursive_generator.hpp> | Flat iteration over recursive structures |
| sync_wait(task) | <cppcoro/sync_wait.hpp> | Block synchronous code until a task completes |
| when_all(tasks...) | <cppcoro/when_all.hpp> | Run tasks concurrently, wait for all |
| schedule_on(scheduler, task) | <cppcoro/schedule_on.hpp> | Run a task on a specific scheduler/thread pool |
| io_service | <cppcoro/io_service.hpp> | I/O event loop for file and socket operations |
Boost.Asio — async I/O with coroutines
Boost.Asio is the most widely used C++ async I/O library and has had coroutine support since well before C++20 (first via Boost.Coroutine2, then via the stackful coroutine adaptor, and now natively via C++20 coroutines). The key type is asio::use_awaitable: passing it as a completion token to any Asio async operation converts that operation into a coroutine awaitable. This means the same async_read, async_write, async_connect functions you know from callback-based Asio work directly with co_await — no new API to learn.
#include <boost/asio.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/use_awaitable.hpp>
namespace asio = boost::asio;
using asio::ip::tcp;
// Coroutine handler: reads a request, sends a response
asio::awaitable<void> handle_client(tcp::socket socket)
{
std::string buf(1024, '\0');
// co_await turns the async operation into a suspension point
std::size_t n = co_await socket.async_read_some(
asio::buffer(buf),
asio::use_awaitable
);
std::string response = "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nHello";
co_await asio::async_write(socket, asio::buffer(response), asio::use_awaitable);
}
// Accept loop
asio::awaitable<void> listener()
{
auto executor = co_await asio::this_coro::executor;
tcp::acceptor acceptor{ executor, { tcp::v4(), 8080 } };
while (true) {
tcp::socket socket = co_await acceptor.async_accept(asio::use_awaitable);
// co_spawn launches a coroutine without awaiting it (fire and forget)
asio::co_spawn(
executor,
handle_client(std::move(socket)),
asio::detached
);
}
}
int main()
{
asio::io_context ctx;
asio::co_spawn(ctx, listener(), asio::detached);
ctx.run(); // drives the event loop until all work is done
}The io_context is Asio's event loop. It integrates with the OS I/O notification mechanism (epoll on Linux, kqueue on macOS, IOCP on Windows) and resumes suspended coroutines when their I/O operations complete. Multiple calls to ctx.run() on different threads create a thread pool that all share the same event queue.
| Asio component | Purpose |
|---|---|
| asio::use_awaitable | Completion token that converts any async op to a coroutine awaitable |
| asio::awaitable<T> | The coroutine return type for Asio coroutines |
| asio::co_spawn(executor, coro, token) | Launch a coroutine on an executor without blocking |
| asio::detached | co_spawn token: fire and forget, ignore result/exception |
| asio::this_coro::executor | Awaitable that yields the current coroutine's executor |
| io_context::run() | Blocks and drives the event loop until work is exhausted |
libunifex — senders and receivers
libunifex is Meta's open-source exploration of the senders/receivers model (formerly known as P2300, now standardised in C++26 as std::execution). Rather than coroutines, its primary abstraction is a sender — a lazy description of an async operation — and a receiver — a continuation that handles completion, error, and cancellation signals. Senders compose with algorithms like then, when_all, and schedule_on, and they interoperate with coroutines via task<T> which is built on top of the sender model.
#include <unifex/task.hpp>
#include <unifex/sync_wait.hpp>
#include <unifex/when_all.hpp>
#include <unifex/on.hpp>
#include <unifex/timed_single_thread_context.hpp>
// Coroutines work through unifex::task<T>
unifex::task<int> compute()
{
co_return 42;
}
// Senders compose with algorithms
auto pipeline = unifex::just(1)
| unifex::then([](int x) { return x * 2; })
| unifex::then([](int x) { return x + 1; });
// pipeline is still lazy; nothing runs until sync_wait or co_await
// Cancellation: senders carry cancellation tokens natively
unifex::task<> cancellable_work(unifex::stop_token token)
{
// The scheduler checks the token between operations
co_await some_io_operation();
if (token.stop_requested()) co_return;
co_await another_operation();
}The key difference from cppcoro and Asio is that libunifex separates the what (the sender algorithm chain) from the where and how (the scheduler and executor). This makes it possible to write algorithm chains that are scheduler-agnostic and then choose the execution context at the call site — a GPU executor, a thread pool, or a single-threaded event loop — without changing the algorithm.
std::execution in C++26. If you are targeting C++26, prefer learning the senders model through libunifex now — the concepts transfer directly to the standard version.Choosing a library
Learning coroutine internals, building your own primitives
Build from scratch using Bancila's recipe (promise-type + generator lessons)
Green-field async I/O application (server, client, protocol handler)
Boost.Asio with use_awaitable — mature, cross-platform, huge ecosystem
Pure coroutine logic without I/O (generators, async pipelines, structured concurrency)
cppcoro — production-quality task<T>, when_all, schedule_on, sync_wait
Targeting C++26 std::execution or scheduler-agnostic algorithm composition
libunifex — senders/receivers, maps to std::execution in C++26
Header-only, zero-dependency, C++20 generators only
std::generator (C++23, covered in the next lesson)