File: completion_tokens.qbk

package info (click to toggle)
boost1.88 1.88.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, trixie
  • size: 576,932 kB
  • sloc: cpp: 4,149,234; xml: 136,789; ansic: 35,092; python: 33,910; asm: 5,698; sh: 4,604; ada: 1,681; makefile: 1,633; pascal: 1,139; perl: 1,124; sql: 640; yacc: 478; ruby: 271; java: 77; lisp: 24; csh: 6
file content (239 lines) | stat: -rw-r--r-- 9,589 bytes parent folder | download | duplicates (3)
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]