网站首页> 文章专栏> [重磅]c++20开源协程库--async_simple让异步变得简单
经过多年的酝酿、争论、准备后,协程终于进入c++20标准。C++20协程采纳的是微软提出并主导(源于C#)的无栈协程。很多人反对这个特性,主要槽点包括:难于理解、过于灵活、动态分配导致的性能问题等等。Google对该提案发起了一系列吐槽并尝试给出另外的方案。很遗憾google的方案也存在很多问题,经常需要依赖奇怪的c++特性,而且这些问题并不容易解决,最终无栈协程提案被接受成为C++20标准特性。
很多人认为协程是c++20最重要的特性,因为协程将在未来三到五年内深刻地影响c++异步编程模型,异步网络库的协程化将是大势所趋,因此很有必要掌握c++20协程。
为了便于理解c++20协程,我们需要先了解一些协程的基本概念。
协程并不是一个新的概念,它距今已经有几十年的历史了,也早已存在于许多其它编程语言(python, c#, go)。
协程是一个可以暂停和恢复的函数,是函数的泛化。为什么说协程是函数的泛化呢?我们知道调用一个函数,函数体(function body)是顺序执行的,执行完之后将结果返回给调用者,我们没办法挂起它并稍后恢复它,只能等待它结束。而协程则允许我们把函数挂起,然后在任意需要的时刻去恢复并执行函数体,相比普通函数,协程的函数体不一定是顺序执行的,可以挂起并在任意时刻恢复执行,所以从这个角度来说,协程对函数调用做了泛化。
围绕着协程是什么的问题,存在很多误解。取决于不同的协程使用场景、实现机制,协程又可能被称为:stackful coroutines, green threads, fibres, goroutines, 实际上它们是同一种事物,虽然使用上有所不同,它们的实现机制都是有栈的。我们统一称呼它们为fibers或stackful coroutines。而我们要介绍的c++20协程则是无栈协程(stackless coroutines)。
协程是一个可以暂停和恢复的函数,它只能被线程调用,本身并不抢占调度,它通常属于某个线程,而线程则可抢占调度。
(注:如果你不想了解c++20协程细节的话可以跳过本节,跳转到 [为什么需要一个协程库]一节)。
c++20协程给我们提供了什么:
编译器会根据c++协程关键字(co_await, co_yield和co_return)生成协程的模板框架,我们需要定制这个框架中的各个可定制点,实现协程的绝大部分行为。什么是模板框架?什么是可定制点?让我们来看一个简单的例子,通过它来窥探协程内部的一些细节。
Task<int> sum(int a, int b) {
co_return 42;
}
根据c++20协程标准,只要一个函数中存在co_await, co_yield和co_return三个关键字之一,它就是协程,因此这个foo是一个协程,编译器会为foo协程生成一些模板代码。
这三行代码最终大概会生成下面这样的模板代码:
首先需要创建协程,创建协程之后是否挂起则由调用者设置initial_suspend的返回类型来确定。
创建协程的流程大概如下:
foo协程创建后,foo会按照编译器生成的一个模板框架去执行:
{
co_await promise.initial_suspend();
try
{
promise.return_value(42); goto final_suspend;
}
catch (...)
{
promise.unhandled_exception();
}
FinalSuspend:
co_await promise.final_suspend();
}
其中的initial_suspend和final_suspend就是编译器帮助我们生成的代码,协程是否挂起由initial_suspend和final_suspend的返回类型来决定。
在这个模板框架里有一些可定制点:如initial_suspend、final_suspend、unhandled_exception和return_value。 我们可以通过promise的initial_suspend和final_suspend返回类型来控制协程是否挂起,在unhandled_exception里处理异常,在return_value里保存协程返回值。可以看到一个协程的实现需要协程使用者和编译器协作才能实现,使用者需要了解该如何定制这些可定制点以实现对协程的灵活控制。
模板代码里还有co_await,它也会生成一系列模板代码:
{
auto&& value = <expr>;
auto&& awaitable = get_awaitable(promise, static_cast<decltype(value)>(value));
auto&& awaiter = get_awaiter(static_cast<decltype(awaitable)>(awaitable));
if (!awaiter.await_ready())
{
using handle_t = std::experimental::coroutine_handle<P>;
using await_suspend_result_t =
decltype(awaiter.await_suspend(handle_t::from_promise(p)));
<suspend-coroutine>
if constexpr (std::is_void_v<await_suspend_result_t>)
{
awaiter.await_suspend(handle_t::from_promise(p));
<return-to-caller-or-resumer>
}
else
{
static_assert(
std::is_same_v<await_suspend_result_t, bool>,
"await_suspend() must return 'void' or 'bool'.");
if (awaiter.await_suspend(handle_t::from_promise(p)))
{
<return-to-caller-or-resumer>
}
}
<resume-point>
}
return awaiter.await_resume();
}
template<typename P, typename T>
decltype(auto) get_awaitable(P& promise, T&& expr)
{
if constexpr (has_any_await_transform_member_v<P>)
return promise.await_transform(static_cast<T&&>(expr));
else
return static_cast<T&&>(expr);
}
template<typename Awaitable>
decltype(auto) get_awaiter(Awaitable&& awaitable)
{
if constexpr (has_member_operator_co_await_v<Awaitable>)
return static_cast<Awaitable&&>(awaitable).operator co_await();
else if constexpr (has_non_member_operator_co_await_v<Awaitable&&>)
return operator co_await(static_cast<Awaitable&&>(awaitable));
else
return static_cast<Awaitable&&>(awaitable);
}
在co_await的模板框架里我们可以定制await_ready, await_suspend和await_resume等可定制点。
当 await_suspend() 的调用返回时,await_suspend() 的返回值为 void 的版本无条件地将执行转移回协程的调用者/恢复者,而返回值为 bool 的版本允许 awaiter 对象有条件地返回并立即恢复协程,而不返回调用者/恢复者。
await_suspen() 的 bool 返回版本在 awaiter 可能启动异步操作(有时可以同步完成)的情况下非常有用。 在它同步完成的情况下,await_suspend() 方法可以返回 false 以指示应该立即恢复协程并继续执行。
在
在
await_ready() 方法的目的,是允许你在已知操作同步完成而不需要挂起的情况下避免
在
当(或者说如果)暂停的协程最终恢复时,执行将在
await_resume() 方法调用的返回值成为 co_await 表达式的结果。await_resume() 方法也可以抛出异常,在这种情况下异常从 co_await 表达式中抛出。
注意,如果异常从 await_suspen() 抛出,则协程会自动恢复,并且异常会从 co_await 表达式抛出而不调用 await_resume()。
我们看到的foo协程只有两行代码,但它最终生成的是一大堆模板框架代码,需要我们写得一些方法会被嵌入其中,这就是c++20协程的微言大义。
C++20协程像一台精巧的“机器”,虽然复杂,但是它又是非常灵活的,允许我们去定制化它的一些“零件”,通过这些定制化的“零件”我们可以随心所欲的控制这台“机器”,让它帮我们实现任何想法。
可以看到C++20协程还是比较复杂的,它的概念多、细节多、又是编译器生成样版代码,又是根据需要设置可定制点
通过前面的介绍可以看到,C++20协程还是比较复杂的,它的概念多、细节多,又是编译器生成的模板框架,又是一些可定制点,还要了解如何和编译器生成的模板框架如何协作,这些对于普通的使用者来说光理解就比较吃力,更逞论灵活运用了。
这时也可以理解为什么当初google吐槽这样的协程提案难于理解、过于灵活了,然而它的确可以让我们仅需要通过定制化一些方法就可以随心所欲的控制协程。
anyway, 这就是c++20协程,它目前只适合给库作者使用,因为它只提供了一些底层的协程原语和一些协程暂停和恢复的机制,普通用户如果希望使用协程只能依赖协程库,由协程库来屏蔽这些底层细节,提供简单易用的API,然而,由于时间紧c++20并没有提供一个协程库,因此我们迫切需要一个基于c++20协程封装好的简单易用的协程库!
正是在这种背景下,c++20协程库async_simple就应运而生了! 阿里巴巴智能引擎事业部开发的c++20协程库,目前广泛应用于图计算引擎、时序数据库、搜索引擎等在线系统。连续两年经历天猫双十一磨砺,承担了亿级别流量洪峰,具备非常强劲的性能和可靠的稳定性。
async_simple现在已经在github上开源,有了它你在也不用为c++20协程的复杂而苦恼了,它将在异步的世界里带你飞,async_simple值得拥有,赶紧去github上关注一下吧。
接下来我们将介绍如何使用async_simple来简化异步编程。
async_simple提供了丰富的协程组件和简单易用的API,主要有:
关于async_simple的更多介绍和示例,可以看github上的文档。
有了这些常用的丰富的协程组件,我们写异步程序就变得很简单了,我们以asio为例写一个简单的echo server来看看async_simple是如何简化异步编程的。
先来看看asio中的echo server的异步读写的经典写法:
void do_read()
{
auto self(shared_from_this());
socket_.async_read_some(asio::buffer(data_, max_length),
[this, self](std::error_code ec, std::size_t length)
{
if (!ec)
{
do_write(length);
}
});
}
void do_write(std::size_t length)
{
auto self(shared_from_this());
asio::async_write(socket_, asio::buffer(data_, length),
[this, self](std::error_code ec, std::size_t /*length*/)
{
if (!ec)
{
do_read();
}
});
}
asio异步读写需要实现一个递归的异步回调链条,即在异步读的回调中调用异步写,在异步写得回调中调用异步读,这样实现一个递归的异步回调链条,如果不是按此操作则是未定义行为。还要注意的另外一个细节是通过shared_from_this来保证异步安全的回调。总的看下来还是比较复杂的,让我们把异步读写和使用同步接口来读写的代码做一个对比看看。
void session(tcp::socket sock) {
for (;;) {
const size_t max_length = 1024;
char data[max_length];
asio::error_code ec;
auto length = sock.read_some(asio::buffer(data, max_length), ec);
if (ec) {
throw asio::system_error(error);
}
write(sock, asio::buffer(data, length));
}
}
可以看到同步接口的写法是非常简单易懂的,但是同步接口又存在性能问题,如果能像同步接口那样写异步代码该多好啊,这正是async_simple的应用场景啊。
async_simple::coro::Lazy<void> session(tcp::socket sock) {
for (;;) {
const size_t max_length = 1024;
char data[max_length];
auto [error, length] =
co_await async_read_some(sock, asio::buffer(data, max_length));
if (error) {
throw asio::system_error(error);
}
co_await async_write(sock, asio::buffer(data, length));
}
}
用async_simple实现的session和同步接口实现的session在写法上几乎一模一样,简洁易懂,而它却是异步读写,保证了异步的高性能,这就是async_simple的威力!
更多demo可以看async_simple的demo example.
使用 async_simple 中的 Lazy 与 folly 中的 Task 以及 cppcoro 中的 task 进行比较,对无栈协程的创建速度与切换速度进行性能测试。
CPU: Intel(R) Xeon(R) Platinum 8163 CPU @ 2.50GHz
----------------------------------------------------------------------
Benchmark Time CPU Iterations
----------------------------------------------------------------------
Folly TaskChain 195801 ns 193211 ns 3616
cppcoro task_chain 61308 ns 60614 ns 11542
async_simple Lazy_chain 59745 ns 59086 ns 11846
Folly TaskCollectAll 23795927 ns 23555262 ns 30
cppcoro task_when_all 8934768 ns 8829864 ns 79
async_simple Lazy_collectAll 7880137 ns 7785291 ns 90
https://github.com/alibaba/async_simple
https://blog.panicsoftware.com/coroutines-introduction/
地址: www.purecpp.cn
转载请注明出处!
purecpp
一个很酷的modern c++开源社区
purecpp社区自2015年创办以来,以“Newer is Better”为理念,相信新技术可以改变世界,一直致力于现代C++研究、应用和技术创新,期望通过现代C++的技术创新来提高企业生产力和效率。
社区坚持只发表原创技术文章,已经累计发表了一千多篇原创C++技术文章;
组织了十几场的C++沙龙和C++大会,有力地促进了国内外C++开发者之间的技术交流;
开源了十几个现代C++项目,被近百家公司所使用,有力地推动了现代C++在企业中的应用。
期待更多的C++爱好者能参与到社区C++社区的建设中来,一起为现代C++开源项目添砖加瓦,一起完善C++基础设施和生态圈。
微信公众号:purecpp, 社区邮箱: purecpp@163.com
写的真好,值得仔细研读一下。我这里也有一个协程async解决方案:http://www.purecpp.org/detail?id=2298,希望有机会可以一起讨论一下![[偷笑]](http://www.purecpp.org/purecpp/plug/layui/images/face/13.gif)