1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
|
[/
/ Copyright (c) 2003-2025 Christopher M. Kohlhoff (chris at kohlhoff dot com)
/
/ Distributed under the Boost Software License, Version 1.0. (See accompanying
/ file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
/]
[section:completion_tokens Completion Tokens]
[$boost_asio/completion_token_model.png [width 436px]]
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 [link boost_asio.reference.use_future 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 [link boost_asio.reference.use_awaitable
use_awaitable] completion token, the initiating function behaves as though it
is an `awaitable`-based coroutine [footnote The [link boost_asio.reference.awaitable
awaitable] class template is included with the Boost.Asio library as a return type
for C++20 coroutines. These coroutines can be trivially implemented in terms of
other `awaitable`-based coroutines.]. 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 [link boost_asio.reference.yield_context 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.
[$boost_asio/completion_token_transform.png [width 856px]]
To see this in practice, let's use a detached thread to adapt a synchronous
operation into an asynchronous one:[footnote For illustrative purposes only.
Not recommended for real world use!]
template <
completion_token_for<void(error_code, size_t)> /*< The `completion_token_for`
concept checks whether
the user-supplied
completion token will
satisfy the specified
completion signature.
For older versions of
C++, simply use `typename`
here instead. >*/
CompletionToken>
auto async_read_some(
tcp::socket& s,
const mutable_buffer& b,
CompletionToken&& token)
{
auto init = []( /*< 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 `async_result`
trait. >*/
auto completion_handler,
tcp::socket* s,
const mutable_buffer& b)
{
std::thread( /*< The body of the function object spawns a new thread to
perform the operation. >*/
[](
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); /*< Once the operation
completes, the completion
handler is passed the
result. >*/
},
std::move(completion_handler),
s,
b
).detach();
};
return async_result< /*< The `async_result` trait is passed the (decayed)
completion token type, and the completion
signatures of the asynchronous operation. >*/
decay_t<CompletionToken>,
void(error_code, size_t)
>::initiate(
init, /*< Call the trait’s `initiate` member function, first passing
the function object that launches the operation. >*/
std::forward<CompletionToken>(token), /*< Next comes the forwarded
completion token. The trait
implementation will convert
this into a concrete
completion handler. >*/
&s, /*< Finally, pass any additional arguments for the function
object. Assume that these may be decay-copied and moved by the
trait implementation. >*/
b
);
}
In practice we should call the [link boost_asio.reference.async_initiate
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 [link boost_asio.reference.deferred 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)
);
};
}
};
[endsect]
|