通过 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_tuple
或 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
。
默认情况下,被取消的协程的继续执行将从任何后续的 co_await
awaitable<>
对象中触发异常。可以通过使用 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_error, awaitable, use_awaitable_t, use_awaitable, this_coro::executor, co_composed, 协程示例, 可恢复的 C++20 协程, 栈式协程, 无栈协程。