Boost C++ 库

...世界上最受推崇和设计最精湛的 C++ 库项目之一。 Herb SutterAndrei Alexandrescu, C++ 编码标准

PrevUpHomeNext

C++20 协程支持

通过 awaitable 类模板、use_awaitable 完成令牌和 co_spawn() 函数提供对 C++20 协程的支持。这些工具允许程序以同步方式实现异步逻辑,并结合 co_await 关键字,如下例所示

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

co_spawn() 的第一个参数是一个 执行器,它决定了允许协程执行的上下文。例如,服务器的每个客户端对象可能由多个协程组成;它们都应该在同一个 strand 上运行,这样就不需要显式的同步。

第二个参数是一个 awaitable<R>,它是协程入口点函数的结果,在上面的例子中是对 echo 的调用的结果。(或者,此参数可以是返回 awaitable<R> 的函数对象。)模板参数 R 是协程产生的返回值类型。在上面的例子中,协程返回 void

第三个参数是一个完成令牌,co_spawn() 使用它来生成一个完成处理程序,其签名为 void(std::exception_ptr, R)。一旦协程完成,就会使用协程的结果调用此完成处理程序。在上面的例子中,我们传递了一个完成令牌类型 boost::asio::detached,它用于显式地忽略异步操作的结果。

在此示例中,协程的主体在 echo 函数中实现。当调用异步操作而没有显式指定完成令牌时,将使用默认完成令牌 deferred。这会导致操作的启动函数返回一个延迟的异步操作对象,该对象可以与 co_await 关键字一起使用

std::size_t n = co_await socket.async_read_some(boost::asio::buffer(data));

或者,我们可以指定 use_awaitable 完成令牌

std::size_t n = co_await socket.async_read_some(boost::asio::buffer(data), boost::asio::use_awaitable);

使用这些完成令牌中的任何一个,当异步操作的处理程序签名具有以下形式时

void handler(boost::system::error_code ec, result_type result);

co_await 表达式的结果类型是 result_type。在上面的 async_read_some 示例中,这是 size_t。如果异步操作失败,则 error_code 将转换为 system_error 异常并抛出。

当处理程序签名具有以下形式时

void handler(boost::system::error_code ec);

co_await 表达式产生 void 结果。与上面一样,错误作为 system_error 异常传递回协程。

错误处理

要执行显式错误处理,而不是默认的抛出异常行为,请使用 as_tupleredirect_error 完成令牌适配器。

as_tuple 完成令牌适配器将完成处理程序参数打包到一个元组中,然后作为等待操作的结果返回。例如

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
    }

    // ...
  }
}

或者,如果显式指定 use_awaitable 完成令牌

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
    }

    // ...
  }
}

结果也可以直接捕获到结构化绑定中

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
    }

    // ...
  }
}

或者,可以使用 redirect_error 完成令牌适配器将错误捕获到提供的 error_code 变量中

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
    }

    // ...
  }
}
协程和每操作取消

co_spawn 创建的所有执行线程都有一个取消状态,该状态记录对协程发出的任何取消请求的当前状态。要访问此状态,请使用 this_coro::cancellation_state,如下所示

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)
    // ...
}

当首次由 co_spawn 创建时,执行线程的取消状态仅支持 cancellation_type::terminal 值。要更改取消状态,请调用 this_coro::reset_cancellation_state

默认情况下,继续执行已取消的协程将从任何后续的 co_awaitawaitable<> 对象触发异常。可以通过使用 this_coro::throw_if_cancelled 来更改此行为。

协调并行协程
[Note] 注意

这是一个实验性功能。

逻辑运算符 ||&& 已为 awaitable<> 重载,以允许并行等待协程。

当使用 && 等待时,co_await 表达式会等待直到两个操作都成功完成。作为“短路”求值,如果一个操作因异常而失败,则另一个操作会立即取消。例如

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

&& 操作完成后,所有操作的结果都连接到一个元组中。在上面的例子中,第一个 size_t 表示 async_read 结果的非异常组件,第二个 size_tasync_write 的结果。

当使用 || 等待时,co_await 表达式会等待直到任一操作成功。作为“短路”求值,如果一个操作成功完成且没有抛出异常,则另一个操作会立即取消。例如

std::variant<std::size_t, std::monostate> results =
  co_await (
    async_read(socket, input_buffer, use_awaitable)
      || timer.async_wait(use_awaitable)
  );

|| 操作完成后,第一个非异常完成的操作的结果将放入 std::variant 中。变体的活动索引反映了哪个操作首先完成。在上面的示例中,索引 0 对应于 async_read 操作。

可以通过添加 #include 来启用这些运算符

#include <boost/asio/experimental/awaitable_operators.hpp>

然后将 experimental::awaitable_operators 命名空间的内容引入作用域

using namespace boost::asio::experimental::awaitable_operators;

注意:要使用这些运算符,我们必须显式指定 use_awaitable 完成令牌。

轻量级协程实现异步操作

co_composed 模板有助于使用 C++20 协程轻量级实现用户定义的异步操作。以下示例说明了一个简单的异步操作,该操作根据协程实现回显协议

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, 协程示例, 可恢复的 C++20 协程, Stackful 协程, Stackless 协程.


PrevUpHomeNext