Boost.Asio 的异步模型的一个关键目标是支持多种组合机制。这通过一个 完成令牌 来实现,用户将其传递给异步操作的启动函数,以自定义库的 API 表面。根据约定,完成令牌是异步操作启动函数的最后一个参数。
例如,如果用户将 lambda(或其他函数对象)作为完成令牌传递,异步操作将如前所述运行:操作开始,当操作完成时,结果将传递给 lambda。
socket.async_read_some(buffer, [](error_code e, size_t) { // ... } );
当用户传递 use_future 完成令牌时,操作将如同使用 promise 和 future 对来实现一样运行。启动函数不仅启动操作,还会返回一个 future,可用于等待结果。
future<size_t> f = socket.async_read_some( buffer, use_future ); // ... size_t n = f.get();
同样,当用户传递 use_awaitable 完成令牌时,启动函数将如同一个基于 awaitable 的协程 [6]。但是,在这种情况下,启动函数不会直接启动异步操作。它只返回 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 启动函数的单个实现支持。
为此,异步操作必须首先指定一个 完成签名(或可能多个签名),该签名描述将传递给其完成处理程序的参数。
然后,操作的启动函数会获取完成签名、完成令牌及其内部实现,并将它们传递给 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); }
我们可以将完成令牌视为一种 proto 完成处理程序。当我们把函数对象(如 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) ); }; } };