How to Use C++20 Coroutines with Asio Awaitable: A Complete Guide
Use asio::awaitable<T> as your coroutine return type, pass asio::use_awaitable as the completion token to asynchronous operations, and launch coroutines with asio::co_spawn to write asynchronous I/O code using natural C++20 co_await syntax.
The Asio library (chriskohlhoff/asio) provides first-class support for C++20 coroutines through a dedicated integration layer that transforms callback-based asynchronous operations into suspendible sequential code. This architecture centers on three key components that work together to bridge Asio’s executor model with the C++20 coroutine language feature.
The Three Core Components of Asio Coroutines
Asio’s coroutine integration is built on a triangular relationship between the return type, completion token, and spawning mechanism. Understanding how these pieces interact is essential for writing correct asynchronous code.
asio::awaitable - The Coroutine Return Type
The asio::awaitable<T> template class serves as the return type for coroutine functions. Defined in [include/asio/awaitable.hpp](https://github.com/chriskohlhoff/asio/blob/master/include/asio/awaitable.hpp) (lines 50-53), this lightweight wrapper stores a pointer to an internal awaitable frame that drives the coroutine’s state machine. When your function returns awaitable<T>, the compiler generates the coroutine infrastructure that Asio manages.
asio::use_awaitable - The Completion Token
The asio::use_awaitable completion token, declared in [include/asio/use_awaitable.hpp](https://github.com/chriskohlhoff/asio/blob/master/include/asio/use_awaitable.hpp) (lines 36-42), instructs Asio’s initiating functions to suspend the current coroutine and resume it when the asynchronous operation completes. This token transforms traditional callback-based APIs into awaitable expressions.
asio::co_spawn - The Coroutine Launcher
asio::co_spawn is the convenience function that creates a new coroutine "thread" on a given executor. Implemented in [include/asio/co_spawn.hpp](https://github.com/chriskohlhoff/asio/blob/master/include/asio/co_spawn.hpp), this function takes an awaitable<T> produced by a user-defined coroutine and attaches it to an execution context, optionally accepting a completion handler for results or exceptions.
Writing Your First Asio Coroutine
A coroutine is a normal C++ function that returns asio::awaitable<T> (or awaitable<void>). Inside the function, use co_await together with Asio’s async operations, passing asio::use_awaitable as the completion token:
#include <asio.hpp>
#include <cstddef>
asio::awaitable<std::size_t> echo(asio::ip::tcp::socket socket)
{
std::size_t total = 0;
char data[1024];
for (;;)
{
// Suspend the coroutine until the read completes.
std::size_t n = co_await socket.async_read_some(
asio::buffer(data), asio::use_awaitable);
// Echo the data back.
co_await asio::async_write(socket,
asio::buffer(data, n), asio::use_awaitable);
total += n;
}
co_return total; // Return value becomes the awaitable’s result.
}
The co_await expression suspends the coroutine at the point of the asynchronous call, yielding control back to the executor until the I/O operation completes.
Launching Coroutines with co_spawn
To execute a coroutine, you must explicitly launch it onto an executor using asio::co_spawn. This function accepts an execution context, the awaitable returned by your coroutine, and a completion handler:
#include <asio.hpp>
#include <iostream>
#include <memory>
int main()
{
asio::io_context ctx;
asio::ip::tcp::acceptor acceptor(ctx,
asio::ip::tcp::endpoint(asio::ip::tcp::v4(), 12345));
// Launch the coroutine.
asio::co_spawn(ctx,
[&]() -> asio::awaitable<void>
{
asio::ip::tcp::socket sock = co_await acceptor.async_accept(asio::use_awaitable);
std::size_t bytes = co_await echo(std::move(sock));
std::cout << "Transferred " << bytes << " bytes\n";
},
[](std::exception_ptr ep, std::size_t transferred)
{
if (ep) std::rethrow_exception(ep);
std::cout << "Echoed " << transferred << " bytes total\n";
});
ctx.run();
}
The completion handler in co_spawn receives two arguments: an std::exception_ptr for any exceptions thrown inside the coroutine, and the value returned by co_return. According to the source in co_spawn.hpp (lines 50-66), this overload handles the result value explicitly. A variant that ignores the result (lines 86-102) accepts a signature of void(std::exception_ptr).
Simplifying Code with Default Completion Tokens
Writing asio::use_awaitable on every asynchronous call becomes repetitive. You can adapt an I/O object to use that token as its default using asio::use_awaitable.as_default_on():
asio::awaitable<void> periodic_timer(asio::steady_timer timer)
{
for (int i = 0; i < 5; ++i)
{
co_await timer.async_wait(); // No token needed here
std::cout << "Tick " << i << "\n";
timer.expires_after(std::chrono::seconds(1));
}
}
int main()
{
asio::io_context ctx;
asio::steady_timer tm(ctx, std::chrono::seconds(1));
// Adapt the timer to use use_awaitable by default.
auto timer = asio::use_awaitable.as_default_on(std::move(tm));
asio::co_spawn(ctx, periodic_timer(std::move(timer)), asio::detached);
ctx.run();
}
The adaptation logic lives in use_awaitable.hpp (lines 94-102), which defines executor_with_default and the as_default_on member function. Once adapted, every async operation on the object automatically uses use_awaitable.
Handling Cancellation in Coroutines
Coroutines inherit a cancellation state that initially supports only terminal cancellation types. You can modify this behavior from within the coroutine using asio::this_coro::reset_cancellation_state, as referenced in co_spawn.hpp (lines 103-108). This allows you to opt into finer-grained cancellation policies such as partial cancellation or total cancellation.
Complete Working Examples
These examples compile with any C++20-compliant compiler using the Asio headers from the chriskohlhoff/asio repository (e.g., g++ -std=c++20 -Iinclude).
TCP Echo Server
#include <asio.hpp>
#include <iostream>
asio::awaitable<std::size_t> echo(asio::ip::tcp::socket socket)
{
std::size_t total = 0;
char data[1024];
for (;;)
{
std::size_t n = co_await socket.async_read_some(
asio::buffer(data), asio::use_awaitable);
co_await asio::async_write(socket,
asio::buffer(data, n), asio::use_awaitable);
total += n;
}
co_return total;
}
int main()
{
asio::io_context ctx;
asio::ip::tcp::acceptor acc(ctx,
asio::ip::tcp::endpoint(asio::ip::tcp::v4(), 5555));
asio::co_spawn(ctx,
[&]() -> asio::awaitable<void>
{
asio::ip::tcp::socket sock = co_await acc.async_accept(asio::use_awaitable);
std::size_t bytes = co_await echo(std::move(sock));
std::cout << "Session finished, " << bytes << " bytes echoed\n";
},
asio::detached);
ctx.run();
}
Periodic Timer with Default Token
#include <asio.hpp>
#include <iostream>
asio::awaitable<void> ticker(asio::steady_timer timer)
{
for (int i = 0; i < 5; ++i)
{
co_await timer.async_wait(); // Uses default token
std::cout << "Tick " << i << "\n";
timer.expires_after(std::chrono::seconds(1));
}
}
int main()
{
asio::io_context ctx;
asio::steady_timer tm(ctx, std::chrono::seconds(1));
auto timer = asio::use_awaitable.as_default_on(std::move(tm));
asio::co_spawn(ctx, ticker(std::move(timer)), asio::detached);
ctx.run();
}
Key Source Files and Implementation Details
Understanding the implementation helps debug complex scenarios:
include/asio/awaitable.hpp- Definesasio::awaitable<T>and its coroutine frame handling (lines 50-53).include/asio/use_awaitable.hpp- Declares theuse_awaitablecompletion token and theas_default_onadapter (lines 36-42, 94-102).include/asio/co_spawn.hpp- Implements the spawning logic with multiple completion handler signatures (lines 50-66, 86-102) and cancellation state support (lines 103-108).include/asio/detail/awaitable_frame.hpp- Contains the internal state machine struct used byawaitable.
Summary
- Return Type - Use
asio::awaitable<T>for coroutine functions that produce values. - Completion Token - Pass
asio::use_awaitableto async operations to enableco_awaitsuspension. - Launch Mechanism - Use
asio::co_spawnto attach coroutines to executors and handle completion or errors. - Default Tokens - Apply
asio::use_awaitable.as_default_on()to I/O objects to reduce boilerplate. - Cancellation - Modify cancellation behavior with
asio::this_coro::reset_cancellation_stateinside coroutines.
Frequently Asked Questions
What compiler flags are needed for Asio C++20 coroutines?
Compile with -std=c++20 (or /std:c++20 on MSVC) and include the Asio header path. The library requires compiler support for the coroutine TS (generally available in GCC 10+, Clang 14+, and MSVC 2019+).
How do I handle errors in Asio coroutines?
Exceptions thrown inside coroutines propagate to the completion handler passed to asio::co_spawn as an std::exception_ptr. Alternatively, use asio::as_tuple(asio::use_awaitable) to receive error codes directly without exceptions.
Can I use Asio coroutines with multiple threads?
Yes. Coroutines launched via co_spawn run on the provided executor. If that executor represents a thread pool (such as asio::thread_pool), the coroutine will execute on those threads. The coroutine itself remains single-threaded unless you explicitly introduce synchronization.
What is the difference between use_awaitable and use_future?
asio::use_awaitable integrates with C++20 coroutines and suspends the current coroutine, while asio::use_future returns a std::future suitable for blocking waits or future continuations. use_awaitable provides lower latency and better integration with Asio’s executor model compared to use_future.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →