表面上我是想说 c++20 的协程, 其实我是想说, asio 封装后的 asio::awaitable<> , 这个威力巨大的武器将是未来异步的基石.
asio::awaitable<> 的概念, 是万物皆可 await. 万物皆可 await 的概念实际上来自 nodejs.
当初, nodejs 用回调地狱带火了异步. 既然回调地狱为何又火? 因为 nodejs 万物只能异步. nodejs 没有除了异步以外的 IO. 于是 nodejs 逼迫大家在异步的道路上进行探索, 终于诞生了 Promsie/await 对. Promise 是库, await 是语言关键字. Promise 先出, await 后出. 没有 await 的 Promise 是不完整的 Promsie , 没有 Promise 的 await 是无法使用的废物. 两个天造地设的一对, 居然并不是同时出生.
Promise 的概念就是, 所有的函数, 都不直接返回数据, 而是返回 Promise , 要取得真正的返回值, 就要 await 这个 Promise. 那么, 调用返回 Promise 函数的地方, 就变成了 initiator , 而 await promsie 的地方, 就是 completion handler. 也就是, Promsie/await 的概念, 正好契合了 Proactor 的异步并发模型. 自从有了 promise/await , 原先为了避免回调地狱而 设计为 on(‘data’, lisener) 的 reactor 模式的库纷纷被抛弃, 大家都要选择 Promise 版的替代品.
Proactor 模型中实现的最好的库, 自然就是 boost.asio, 或者说,是 std::net.
asio 最初对异步的支持, 使用的是 initiator + completion handler 的模式. 同 nodejs 一样, 陷入了 回调地狱. 但是很快, asio 发明了 stackless 协程. 实际就是 Duff’s Device 实现的可重入函数. 基本用法是
reenter(this)
{
yield asio::async_read(socket, *this);
}
reenter 和 yield 都是宏, 展开后, 就是变成了一个 for循环+switch case 嵌套.
但是, 这种使用宏展开变成 Duff’s Device 的可重入函数, 不如语言层面的支持来的利索干净. 因此 asio 作者给标准委员会提案, 干脆把宏变成关键字. 有了编译器的支持, Duff’s Device 带来的一些缺陷就可以规避和修正. 使用上也会更便利.
然而这样的优雅提案, 并没有别接受. 委员会转而支持了微软提交的 co_await 协程.
微软缺乏异步大师, 他们有了 co_await 武器, 只能设计出 winrt::IAsyncResult 这样的不能包含万物的协程.
而 asio 爸爸不一样, 它捣鼓出了 asio::awaitable<>
万物皆可 await, 使用的方法和 nodejs 如出一辙.
在 nodejs 里, Promise/await 的使用方法很简单, 首先是将原先的函数声明为 async , 然后就可以在函数体里使用 await .
声明为 async 的函数, 自身也自动变成了 Promise, 可以被其他函数 await
万物皆可 await,
到了 asio 这里, 只要把原来 T 返回值的函数, 换成 asio::awaitable<T> , 就可以在函数体里使用 co_await, 并能被其他函数 co_await.
万物皆可 co_await 后, 我连程序入口点都改成了 asio::awaitable
然后写一个简单的 stub main
asio::awaitable<int> co_main(int argc, char * argv)
{
std::cout << "hello world\n";
co_return 0;
}
int main(int argc, char * argv)
{
int co_main_ret;
asio::io_context io;
asio::co_spawn(io, co_main(argc, argv), [&co_main_ret](std::exception_ptr e_ptr, int co_main_return)
{
if (e_ptr)
std::rethrow_exeption(e_ptr);
co_main_ret = co_main_return;
});
io.run();
return co_main_ret;
}
这样 co_main 就变成了真正的 main.
co_main 因为本身处于协程之中, 因此, 他可以使用 co_await asio::this_coro::executor 获取 executor 从而构造需要的 IO 对象.
比如这样写
asio::awaitable<int> co_main(int argc, char * argv)
{
std::cout << "hello ";
asio::steady_clock timer(co_await asio::this_coro::executor);
timer.expires_from_now(1s);
co_await timer.async_await(asio::use_awaitable);
"world\n";
co_return 0;
}
int main(int argc, char * argv)
{
int co_main_ret;
asio::io_context io;
asio::co_spawn(io, co_main(argc, argv), [&co_main_ret](std::exception_ptr e_ptr, int co_main_return)
{
if (e_ptr)
std::rethrow_exeption(e_ptr);
co_main_ret = co_main_return;
});
io.run();
return co_main_ret;
}
调用原先需要传递 回调函数的 asio异步函数, 只要把回调换成 asio::use_awaitable 占位符, 就可以自动把待调用函数变成 awaitable.
因此, 回调不再是回调了, asio 的文档都已经把 callback handler 改称为 completion token 了. 传递 use_awaitable 当做 completion token, 则 initiator 函数就自动 awaitable.
asio 需要兼容 回调/co_await 两种模式, 所以使用了 completion token 概念, 而我自己的代码, 不需要这个概念, 我只要全部 co_await 话, 把所有的函数, 统统变成 awaitable.
万物皆 awaitable 后, 我发现了新大陆.
原先的设计思路突然被放开了限制, 豁然开朗.
原先我设计的程序主体启动代码是这样的
my_server srv(io, configs);
srv.start();
asio::signal_set s(io, SIGTERM);
s.async_wait([&](auto ec, auto sig){ srv.stop() ;} )
io.run();
变成 awaitable 后, 我的主体代码是这样的
my_server srv(io, configs);
asio::signal_set sighandler(io, SIGTERM);
co_await ( srv.run() || sighandler.async_await(asio::use_awaitable) ) ;
co_await srv.stop();
而且, srv.stop() 变成 awaitable 后, 更容易实现优雅的退出了, 因为可以等待一些费时的逻辑完成退出后, stop 才会 resolve.
这样, srv 就在干净的状态下析构, co_main 也顺利干净利落的返回.
Comments