...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
A key goal of Boost.Asio's asynchronous model is to support multiple composition mechanisms. This is achieved via a completion token, which the user passes to an asynchronous operation's initiating function to customise the API surface of the library. By convention, the completion token is the final argument to an asynchronous operation's initiating function.
For example, if the user passes a lambda (or other function object) as the completion token, the asynchronous operation behaves as previously described: the operation begins, and when the operation completes the result is passed to the lambda.
socket.async_read_some(buffer, [](error_code e, size_t) { // ... } );
When the user passes the use_future
completion token, the operation behaves as though implemented in terms
of a promise
and future
pair. The initiating function
does not just launch the operation, but also returns a future that may
be used to await the result.
future<size_t> f = socket.async_read_some( buffer, use_future ); // ... size_t n = f.get();
Similarly, when the user passes the use_awaitable
completion token, the initiating function behaves as though it is an awaitable
-based coroutine [6]. However, in this case the initiating function does not launch
the asynchronous operation directly. It only returns the awaitable
, which in turn launches the
operation when it is co_await-ed.
awaitable<void> foo() { size_t n = co_await socket.async_read_some( buffer, use_awaitable ); // ... }
Finally, the yield_context completion token causes the initiating function to behave as a synchronous operation within the context of a stackful coroutine. It not only begins the asynchronous operation, but blocks the stackful coroutine until it is complete. From the point of the stackful coroutine, it is a synchronous operation.
void foo(boost::asio::yield_context yield) { size_t n = socket.async_read_some(buffer, yield); // ... }
All of these uses are supported by a single implementation of the async_read_some
initiating function.
To achieve this, an asynchronous operation must first specify a completion signature (or, possibly, signatures) that describes the arguments that will be passed to its completion handler.
Then, the operation's initiating function takes the completion signature,
completion token, and its internal implementation and passes them to the
async_result trait. The async_result
trait is a customisation point that combines these to first produce a concrete
completion handler, and then launch the operation.
To see this in practice, let's use a detached thread to adapt a synchronous operation into an asynchronous one:[7]
template < completion_token_for<void(error_code, size_t)> CompletionToken> auto async_read_some( tcp::socket& s, const mutable_buffer& b, CompletionToken&& token) { auto init = []( auto completion_handler, tcp::socket* s, const mutable_buffer& b) { std::thread( []( auto completion_handler, tcp::socket* s, const mutable_buffer& b ) { error_code ec; size_t n = s->read_some(b, ec); std::move(completion_handler)(ec, n); }, std::move(completion_handler), s, b ).detach(); }; return async_result< decay_t<CompletionToken>, void(error_code, size_t) >::initiate( init, std::forward<CompletionToken>(token), &s, b ); }
The |
|
Define a function object that contains the code to launch the asynchronous
operation. This is passed the concrete completion handler, followed
by any additional arguments that were passed through the |
|
The body of the function object spawns a new thread to perform the operation. |
|
Once the operation completes, the completion handler is passed the result. |
|
The |
|
Call the trait’s |
|
Next comes the forwarded completion token. The trait implementation will convert this into a concrete completion handler. |
|
Finally, pass any additional arguments for the function object. Assume that these may be decay-copied and moved by the trait implementation. |
In practice we should call the async_initiate
helper function, rather than use the async_result
trait directly. The async_initiate
function automatically performs the necessary decay and forward of the
completion token, and also enables backwards compatibility with legacy
completion token implementations.
template < completion_token_for<void(error_code, size_t)> CompletionToken> auto async_read_some( tcp::socket& s, const mutable_buffer& b, CompletionToken&& token) { auto init = /* ... as above ... */; return async_initiate< CompletionToken, void(error_code, size_t) >(init, token, &s, b); }
We can think of the completion token as a kind of proto completion handler. In the case where we pass a function object (like a lambda) as the completion token, it already satisfies the completion handler requirements. The async_result primary template handles this case by trivially forwarding the arguments through:
template <class CompletionToken, completion_signature... Signatures> struct async_result { template < class Initiation, completion_handler_for<Signatures...> CompletionHandler, class... Args> static void initiate( Initiation&& initiation, CompletionHandler&& completion_handler, Args&&... args) { std::forward<Initiation>(initiation)( std::forward<CompletionHandler>(completion_handler), std::forward<Args>(args)...); } };
We can see here that this default implementation avoids copies of all arguments, thus ensuring that eager initiation is as efficient as possible.
On the other hand, a lazy completion token (such as use_awaitable
above) may capture these arguments for deferred launching of the operation.
For example, a specialisation for a trivial deferred
token (that simply packages the operation for later) might look something
like this:
template <completion_signature... Signatures> struct async_result<deferred_t, Signatures...> { template <class Initiation, class... Args> static auto initiate(Initiation initiation, deferred_t, Args... args) { return [ initiation = std::move(initiation), arg_pack = std::make_tuple(std::move(args)...) ](auto&& token) mutable { return std::apply( [&](auto&&... args) { return async_result<decay_t<decltype(token)>, Signatures...>::initiate( std::move(initiation), std::forward<decltype(token)>(token), std::forward<decltype(args)>(args)... ); }, std::move(arg_pack) ); }; } };