Boost.Asio 异步模型的一个关键目标是支持多种组合机制。这是通过一个 完成令牌 (completion token) 来实现的,用户将其传递给异步操作的启动函数,以自定义库的 API 表面。按照约定,完成令牌是异步操作启动函数的最后一个参数。
例如,如果用户将 lambda(或其他函数对象)作为完成令牌传递,异步操作将如前所述执行:操作开始,当操作完成时,结果将被传递给 lambda。
socket.async_read_some(buffer, [](error_code e, size_t) { // ... } );
当用户传递 use_future 完成令牌时,操作的行为就像是基于 promise
和 future
对实现的。启动函数不仅会启动操作,还会返回一个 future,该 future 可用于等待结果。
future<size_t> f = socket.async_read_some( buffer, use_future ); // ... size_t n = f.get();
类似地,当用户传递 use_awaitable 完成令牌时,启动函数的行为就像是一个基于 awaitable
的协程 [6]。然而,在这种情况下,启动函数不会直接启动异步操作。它只返回 awaitable
,当 awaitable
被 co_await 时,它才会启动操作。
awaitable<void> foo() { size_t n = co_await socket.async_read_some( buffer, use_awaitable ); // ... }
最后,yield_context 完成令牌会使启动函数在堆栈式协程的上下文中表现得像一个同步操作。它不仅会开始异步操作,还会阻塞堆栈式协程直到操作完成。从堆栈式协程的角度来看,这是一个同步操作。
void foo(boost::asio::yield_context yield) { size_t n = socket.async_read_some(buffer, yield); // ... }
所有这些用法都由 async_read_some
启动函数的单一实现支持。
为了实现这一点,异步操作必须首先指定一个 完成签名 (completion signature)(或可能不止一个),该签名描述将传递给其完成处理程序的参数。
然后,操作的启动函数会获取完成签名、完成令牌及其内部实现,并将它们传递给 async_result 特性。 async_result
特性是一个自定义点,它将这些结合起来,首先生成一个具体的完成处理程序,然后启动操作。
为了在实践中看到这一点,让我们使用一个独立的线程将同步操作适配为异步操作:[7]
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 = [](
auto completion_handler, tcp::socket* s, const mutable_buffer& b) { std::thread(
[]( 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);
}, std::move(completion_handler), s, b ).detach(); }; return async_result<
decay_t<CompletionToken>, void(error_code, size_t) >::initiate( init,
std::forward<CompletionToken>(token),
&s,
b ); }
|
|
定义一个包含启动异步操作代码的函数对象。它会接收具体的完成处理程序,然后是传递给 |
|
函数对象的体部分会生成一个新线程来执行操作。 |
|
操作完成后,完成处理程序将接收结果。 |
|
|
|
调用特性的 |
|
接下来是转发的完成令牌。特性实现会将此转换为具体的完成处理程序。 |
|
最后,传递函数对象的任何附加参数。假设这些参数可以由特性实现进行衰减复制和移动。 |
实际上,我们应该调用 async_initiate 辅助函数,而不是直接使用 async_result
特性。 async_initiate
函数会自动执行必要的衰减和完成令牌的转发,并启用向后兼容旧的完成令牌实现。
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); }
我们可以将完成令牌视为一种原完成处理程序。在我们传递函数对象(如 lambda)作为完成令牌的情况下,它已经满足了完成处理程序的要求。async_result 主模板通过简单地转发参数来处理这种情况。
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)...); } };
我们可以在这里看到,这个默认实现避免了所有参数的拷贝,从而确保了高效的即时启动。
另一方面,懒惰的完成令牌(如上面的 use_awaitable
)可能会捕获这些参数以延迟启动操作。例如,对于一个简单的 deferred 令牌(它只是将操作打包以便稍后执行)的特化可能看起来像这样:
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) ); }; } };