...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
Support for C++20 Coroutines is provided via the awaitable
class template, the
use_awaitable
completion token, and the co_spawn()
function. These facilities allow programs to implement asynchronous logic
in a synchronous manner, in conjunction with the co_await
keyword, as shown in the following example:
boost::asio::co_spawn(executor, echo(std::move(socket)), boost::asio::detached); // ... boost::asio::awaitable<void> echo(tcp::socket socket) { try { char data[1024]; for (;;) { std::size_t n = co_await socket.async_read_some(boost::asio::buffer(data)); co_await async_write(socket, boost::asio::buffer(data, n)); } } catch (std::exception& e) { std::printf("echo Exception: %s\n", e.what()); } }
The first argument to co_spawn()
is an executor
that determines the context in which the coroutine is permitted to execute.
For example, a server's per-client object may consist of multiple coroutines;
they should all run on the same strand
so that no explicit synchronisation is required.
The second argument is an awaitable<R>
,
that is the result of the coroutine's entry point function, and in the
above example is the result of the call to echo
.
(Alternatively, this argument can be a function object that returns the
awaitable<R>
.) The template parameter R
is the type of return value produced
by the coroutine. In the above example, the coroutine returns void
.
The third argument is a completion token, and this is used by co_spawn()
to produce a completion handler with signature void(std::exception_ptr, R)
.
This completion handler is invoked with the result of the coroutine once
it has finished. In the above example we pass a completion token type,
boost::asio::detached
,
which is used to explicitly ignore the result of an asynchronous operation.
In this example the body of the coroutine is implemented in the echo
function. When an asynchronous operation
is called without explicitly specifying a completion token, the default
completion token deferred
is used. This causes
the operation's initiating function to return a deferred asynchronous operation
object that may be used with the co_await
keyword:
std::size_t n = co_await socket.async_read_some(boost::asio::buffer(data));
Alternatively, we can specify the use_awaitable
completion token:
std::size_t n = co_await socket.async_read_some(boost::asio::buffer(data), boost::asio::use_awaitable);
With either of those completion tokens, when an asynchronous operation's handler signature has the form:
void handler(boost::system::error_code ec, result_type result);
the resulting type of the co_await
expression is result_type
.
In the async_read_some
example above, this is size_t
.
If the asynchronous operation fails, the error_code
is converted into a system_error
exception and thrown.
Where a handler signature has the form:
void handler(boost::system::error_code ec);
the co_await
expression
produces a void
result. As
above, an error is passed back to the coroutine as a system_error
exception.
To perform explicit error handling, rather than the default exception-throwing
behaviour, use the as_tuple
or redirect_error
completion token
adapters.
The as_tuple
completion
token adapter packages the completion handler arguments into a single tuple,
which is then returned as the result of the awaited operation. For example:
boost::asio::awaitable<void> echo(tcp::socket socket) { char data[1024]; for (;;) { std::tuple<boost::system::error_code, std::size_t> result = co_await socket.async_read_some( boost::asio::buffer(data), boost::asio::as_tuple); if (!std::get<0>(result)) { // success } // ... } }
or if explicitly specifying the use_awaitable
completion token:
boost::asio::awaitable<void> echo(tcp::socket socket) { char data[1024]; for (;;) { std::tuple<boost::system::error_code, std::size_t> result = co_await socket.async_read_some(boost::asio::buffer(data), boost::asio::as_tuple(boost::asio::use_awaitable)); if (!std::get<0>(result)) { // success } // ... } }
The result can also be captured directly into a structured binding:
boost::asio::awaitable<void> echo(tcp::socket socket) { char data[1024]; for (;;) { auto [ec, n] = co_await socket.async_read_some( boost::asio::buffer(data), boost::asio::as_tuple); if (!ec) { // success } // ... } }
Alternatively, the redirect_error
completion token adapter may be used to capture the error into a supplied
error_code
variable:
boost::asio::awaitable<void> echo(tcp::socket socket) { char data[1024]; for (;;) { boost::system::error_code ec; std::size_t n = co_await socket.async_read_some( boost::asio::buffer(data), boost::asio::redirect_error(ec)); if (!ec) { // success } // ... } }
All threads of execution created by co_spawn
have a cancellation state that records the current state of any cancellation
requests made to the coroutine. To access this state, use this_coro::cancellation_state
as follows:
boost::asio::awaitable<void> my_coroutine() { boost::asio::cancellation_state cs = co_await boost::asio::this_coro::cancellation_state; // ... if (cs.cancelled() != boost::asio::cancellation_type::none) // ... }
When first created by co_spawn
,
the thread of execution has a cancellation state that supports cancellation_type::terminal
values only. To change the cancellation
state, call this_coro::reset_cancellation_state
.
By default, continued execution of a cancelled coroutine will trigger an
exception from any subsequent co_await
of an awaitable<>
object. This behaviour can be changed by using this_coro::throw_if_cancelled
.
![]() |
Note |
---|---|
This is an experimental feature. |
The logical operators ||
and
&&
have been overloaded
for awaitable<>
,
to allow coroutines to be trivially awaited in parallel.
When awaited using &&
,
the co_await
expression
waits until both operations have completed successfully. As a "short-circuit"
evaluation, if one operation fails with an exception, the other is immediately
cancelled. For example:
std::tuple<std::size_t, std::size_t> results = co_await ( async_read(socket, input_buffer, use_awaitable) && async_write(socket, output_buffer, use_awaitable) );
Following completion of a &&
operation, the results of all operations are concatenated into a tuple.
In the above example, the first size_t
represents the non-exceptional component of the async_read
result, and the second size_t
is the result of the async_write
.
When awaited using ||
, the
co_await
expression waits
until either operation succeeds. As a "short-circuit" evaluation,
if one operation succeeds without throwing an exception, the other is immediately
cancelled. For example:
std::variant<std::size_t, std::monostate> results = co_await ( async_read(socket, input_buffer, use_awaitable) || timer.async_wait(use_awaitable) );
Following completion of a ||
operation, the result of the first operation to complete non-exceptionally
is placed into a std::variant
. The active index of the variant
reflects which of the operations completed first. In the above example,
index 0
corresponds to the
async_read
operation.
These operators may be enabled by adding the #include
:
#include <boost/asio/experimental/awaitable_operators.hpp>
and then bringing the contents of the experimental::awaitable_operators
namespace into scope:
using namespace boost::asio::experimental::awaitable_operators;
Note: To use these operators we must explicitly specify the use_awaitable
completion token.
The co_composed
template
facilitates a lightweight implementation of user-defined asynchronous operations
using C++20 coroutines. The following example illustrates a simple asynchronous
operation that implements an echo protocol in terms of a coroutine:
template <typename CompletionToken> auto async_echo(tcp::socket& socket, CompletionToken&& token) { return boost::asio::async_initiate< CompletionToken, void(boost::system::error_code)>( boost::asio::co_composed< void(boost::system::error_code)>( [](auto state, tcp::socket& socket) -> void { try { state.throw_if_cancelled(true); state.reset_cancellation_state( boost::asio::enable_terminal_cancellation()); for (;;) { char data[1024]; std::size_t n = co_await socket.async_read_some( boost::asio::buffer(data)); co_await boost::asio::async_write(socket, boost::asio::buffer(data, n)); } } catch (const boost::system::system_error& e) { co_return {e.code()}; } }, socket), token, std::ref(socket)); }
co_spawn, detached, as_tuple, redirect_error, awaitable, use_awaitable_t, use_awaitable, this_coro::executor, co_composed, Coroutines examples, Resumable C++20 Coroutines, Stackful Coroutines, Stackless Coroutines.