C++20 协程支持通过 awaitable 类模板、use_awaitable 完成令牌和 co_spawn() 函数提供。这些设施允许程序以同步的方式实现异步逻辑,结合 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() 的第一个参数是一个 executor,它决定了协程允许执行的上下文。例如,服务器的每个客户端对象可能包含多个协程;它们都应该在同一个 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_tuple、redirect_disposition 或 redirect_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。
默认情况下,已取消协程的继续执行将从任何后续的 awaitable<> 对象的 co_await 中触发异常。此行为可以通过使用 this_coro::throw_if_cancelled 来更改。
![]() |
注意 |
|---|---|
这是一个实验性功能。 |
逻辑运算符 || 和 && 已为 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_t 是 async_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_disposition, redirect_error, awaitable, use_awaitable_t, use_awaitable, this_coro::executor, co_composed, 协程示例, 可恢复的 C++20 协程, 栈式协程, 无栈协程.