Boost C++ Libraries

...one of the most highly regarded and expertly designed C++ library projects in the world. Herb Sutter and Andrei Alexandrescu, C++ Coding Standards

PrevUpHomeNext

Completion Tokens

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)> 1
      CompletionToken>
auto async_read_some(
    tcp::socket& s,
    const mutable_buffer& b,
    CompletionToken&& token)
{
  auto init = []( 2
      auto completion_handler,
      tcp::socket* s,
      const mutable_buffer& b)
  {
    std::thread( 3
        [](
            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); 4
        },
        std::move(completion_handler),
        s,
        b
      ).detach();
  };

  return async_result< 5
      decay_t<CompletionToken>,
      void(error_code, size_t)
    >::initiate(
        init, 6
        std::forward<CompletionToken>(token), 7
        &s, 8
        b
      );
}

1

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.

2

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.

3

The body of the function object spawns a new thread to perform the operation.

4

Once the operation completes, the completion handler is passed the result.

5

The async_result trait is passed the (decayed) completion token type, and the completion signatures of the asynchronous operation.

6

Call the trait’s initiate member function, first passing the function object that launches the operation.

7

Next comes the forwarded completion token. The trait implementation will convert this into a concrete completion handler.

8

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)
        );
    };
  }
};


[6] The 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.

[7] For illustrative purposes only. Not recommended for real world use!


PrevUpHomeNext