Boost C++ 库

这是世界上最受推崇、设计最精良的 C++ 库项目之一。 Herb SutterAndrei Alexandrescu, C++ Coding Standards

C++20 协程支持 - Boost C++ 函数库
PrevUpHomeNext

通过 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_await awaitable<> 对象中触发异常。可以通过使用 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 协程, 栈式协程, 无栈协程


PrevUpHomeNext