浅说 c++20 coroutine
浅说cppcoro
上一篇《浅说c/c++ coroutine》介绍了stackful协程,举了win32 Fiber跟tencent/libco为例。
本篇https://www.cnblogs.com/bbqzsl/p/18659948内容则是stackless协程,具体实现为c++20 coroutine,其中代表的库是cppcoro。
先来看编译器将c++代码的协程函数做了什么处理,产生了哪些实际的代码。

就像lambda是一种新语义一样,lambda定义的函数返回一个lambda,它有自己的captured帧,执行体是operator()。协程也是一种新语义。定义它的函数名,变成了一个工厂方法,生成一个协程对象句柄std::coroutine_handle,实质是一个协程帧对象,它有一个执行体actor()函数,这个函数执行协程函数定义的函数体内的代码。我们要向协程对象std::coroutine_handle调用resume()方法或operator()方法,去执行协程帧对象的actor()函数。协程帧对象有一个函数指针__resume,它就是指向这个actor()函数的。
这就是为什么调用协程名()不是执行代码,却是返回一个协程对象的原因。这跟传统的函数定义方式,调用行为有违和感的地方。开始会在感到不适应,是因为不知道细节,用旧思想套新事物。知道细节后,思维切换后其实就很快适应了。
一个协程还依赖两个概念(接口),它们分别是Promise跟Awaitable,编译器使用它们编辑我们的协程代码。虽然没有明确地将它们定义成concept,但是可以等同于概念来看待。在上面的图可以看到编译器将协程函数的代码,用这两个概念的接口组装成一份新的函数代码。协程函数定义依赖Promise概念(接口),co_await关键字依赖Awaitable概念(接口)。
将Promise跟Awaitable定义成concept的话,(Promise跟Awaitable并非一个被定义的c++concept,这里借用concept来描述接口依赖,并且区别于基于继承的抽象类接口), 如下

1 // 定义 Awaitable 概念
2 template <typename T>
3 concept Awaitable = requires(T t, std::coroutine_handle<> h) {
4 { t.await_ready() } -> std::convertible_to<bool>; // 必须有 await_ready 方法
5 { t.await_suspend(h) }; // 必须有 await_suspend 方法
6 { t.await_resume() }; // 必须有 await_resume 方法
7 };
8
9 // 定义 PromiseType 概念
10 template <typename T>
11 concept PromiseType = requires(T promise) {
12 { promise.get_return_object() }; // 必须有 get_return_object 方法
13 { promise.initial_suspend() } -> std::same_as<std::suspend_always>
14 || std::same_as<std::suspend_never>
15 || Awaitable; // initial_suspend 的返回值必须满足要求
16 { promise.final_suspend() } -> std::same_as<std::suspend_always>
17 || std::same_as<std::suspend_never>
18 || Awaitable; // final_suspend 的返回值必须满足要求
19 { promise.unhandled_exception() }; // 必须有 unhandled_exception 方法
20 { promise.return_void() } || { promise.return_value(42) }; // 必须有返回值处理方法
21 };
Promise,Awaitable,concept
编译器使用Promise概念生成的代码套间。

c++ coroutine采用stackless协程实现方式,基于状态机的闭包函数。
下面来看编译器如何将协程函数的代码生成状态机代码

编译器会在每一处co_await调用的地方记下一个新状态。co_await都是一个待定的地方,此处有可能挂起。挂起就意味着协程切换。所以此处必须有一个状态来标记。不同于stackful协程那样使用现场保护进行代码跳转切换,stackless走的另一条路,使用状态机切换代码线路。当协程代码遇到需要挂起时,协程只需要保存当前状态机状态,立即退出函数返回,就可以中止当前代码流实现挂起。恢复协程,则再次重入协程函数,然后根据状态机保存的状态就可以直接切换回原来挂起的代码线路,继续执行。stackful协程是在完全模拟线程切换。线程不但可以通过代码主动切换,还要受中断被切换。被动切换会发生在代码任意位置,所以要保留大量现场信息。但是主动切换的位置,都是代码确定的,所以协程可以用状态机函数来实现切换。上面示例图,example协程,代码流执行到co_await B(),因为B awaiter需要挂起,而挂起example协程。example协程当前状态__resuem_at设置成2,直接退出函数中止执行流。当你恢复example协程时,重入函数,直接跳到__resume_at == 2的地方,这个地方会安排挂起源B的await_resume()方法接收结果,然后继续后面的代码流。这里必需要注意,example恢复时,不会去关心或检测B协程是否完成,所以由挂起源B来控制向协程发出恢复。
然后我们来看协程的闭包实现。虽然stackless协程使用常规函数调用进行切换,但是协程函数的代码局部变量却不能够放在堆栈上。从上面刚介绍完挂起原理可以知道,stackless协程在挂起的情况,直接退出函数,销毁调用帧,平衡堆栈,传统函数放在堆栈的局部变量统统都要被析构。所以协程函数需要是闭包的有自己的局部变量空间。这样才能让函数重入后继续原来的线路执行下去。例如每次co_await的awaiter都是一个局部变量,下次恢复时,还要对awaiter询问状态。所以stackless协程有自己的private coro frame。这是由编译器去实现的。借助编译器,将协程函数的局部变量都分配在协程的private coro frame,在协程函数代码流终止的地方,才会销毁这个私有帧。示图如下

再来看反编译出来的私有帧

私有帧跟协程函数局部变量的对应

接下来看co_await是在做什么的。先来写一个co_await2函数来模拟编译器如何依赖Awaitable概念生成代码的。

co_await2函数,描述的是co_await是如何工作的。
co_await关键字,使用Awaitable概念的三个接口函数来实现await操作。这时要注意,co_await expr语义,跟操作子operator co_await()函数是不同的。await操作是前者语法,后者语法是一个函数用来获取一个awaiter。co_await expr语义,期望expr是一个awaiter对象,这个对象满足Awaitable概念接口。如果expr不是一个awaiter对象,就会调用操作子operator co_await()函数向它索取一个awaiter对象。有了awaiter对象,然后展开代码对其实施await操作。await操作具体包含三个步,awaiter.await_ready()检测,awaiter.await_suspend()挂起,awaiter.await_resume()被恢复前夕。
co_await expr语义,另一个重要的作用,就是告诉编译器设立切换点,生成状态机代码。确保下一次resume,执行流直接在co_await的下一句开始。这也是std::coroutine的局限性,本质上是不支持对称协程的。不同于stackful协程保留调用resume的现场。而stackless协程,用状态机逻辑来模拟一个恢复点,这个恢复点是逻辑决定好的。换句话来说,如果没有一个状态机的恢复点,你并不能够恢复到你切换其它协程的地方。当你恢复这样的一个协程时,actor()函数重入,却从头到尾去执行,或者是从上一个恢复点,而并非你调用resume()的地方。明确地说std::coroutine不可以离开co_await的加持,随意调用其它协程的resume进行切换。这点对于已经习惯写对称协程的你来说,或者你想用它来写对称协程,会是很蛋痛的。


一对比来看,是不是很蛋痛。所以用std::coroutine进行协程切换时,必须依赖co_await让代码生成一个切换点,因此应该谨慎直接使用resume()。尽可能在一个co_await操作里面使用resume(),co_await操作的对象的一个awaiter,所以要尽可能在一个awaitable里面的await_suspend()函数中使用resume()。并且使用void awiat_suspend()重载,让原来的协程中止执行流,因为resume()结束后还会回到原来协程caller/resumer的代码。除此外,我们还可以通过symmetric_transfer方式来避免直接调用resume()。symmetric_transfer方式就是使用awiat_suspend()重载返回一个std::coroutine_handle。由编译器生成代码来调用resume()。所以上面的例子就变成下面的样子

不同于stackful的resume切换cpu上下文,stackless的resume是状态机函数重入,因此每一次resume都要增加一层调用帧。并且每一次resume执行流中止或完成终止,要回到上一层resume的调用帧。这就是stackless协程的资源成本,每一次resume要占用一些堆栈,中止或终止后还逐帧退出而占用几条指令。


可以看到每次resume()调用,要消耗0x30个字节。以2MB的堆栈来看,用上面的对称协程例子,43690次切换就可以crash堆栈。
下面以cppcoro::task为例,它的实现思路是

如果是对称方式的实现如上,非对称方式的实现,final_awaitable::await_suspend() return false,直接回到caller/resumer。


cppcoro有一个编译选项CPPCORO_COMPILER_SUPPORTS_SYMMETRIC_TRANSFER,以供选择实现的方式。需要clang7以上。其它编译器默认不使用。一般来说,能支持c++20的编译器都支持这个选项。是否要开启提供下面样本作为参考。

看来用std::coroutine来搞对称协程,多少有些不自然。
cppcoro留待下篇,前置先介绍std::coroutine。
浅说 c++20 coroutine的更多相关文章
- Lua 协程coroutine
协程和一般多线程的区别是,一般多线程由系统决定该哪个线程执行,是抢占式的,而协程是由每个线程自己决定自己什么时候不执行,并把执行权主动交给下一个线程. 协程是用户空间线程,操作系统其存在一无所知,所以 ...
- (zt)Lua的多任务机制——协程(coroutine)
原帖:http://blog.csdn.net/soloist/article/details/329381 并发是现实世界的本质特征,而聪明的计算机科学家用来模拟并发的技术手段便是多任务机制.大致上 ...
- Tornado源码分析系列之一: 化异步为'同步'的Future和gen.coroutine
转自:http://blog.nathon.wang/2015/06/24/tornado-source-insight-01-gen/ 用Tornado也有一段时间,Tornado的文档还是比较匮乏 ...
- 学习tolua#·20多个例子
初始项目搭建 clone官方库 新建unity工程 依次把官方库里的Assets和Unity5.x/Assets拷贝到项目Assets里 打开unity工程, 开始逐个学习例子,例子目录: 1. he ...
- Lua的多任务机制——协程(coroutine)
并发是现实世界的本质特征,而聪明的计算机科学家用来模拟并发的技术手段便是多任务机制.大致上有这么两种多任务技术,一种是抢占式多任务(preemptive multitasking),它让操作系统来决定 ...
- python enhanced generator - coroutine
本文主要介绍python中Enhanced generator即coroutine相关内容,包括基本语法.使用场景.注意事项,以及与其他语言协程实现的异同. enhanced generator 在上 ...
- asyncio异步IO--协程(Coroutine)与任务(Task)详解
摘要:本文翻译自Coroutines and Tasks,主要介绍asyncio中用于处理协程和任务的方法和接口.在翻译过程中,译者在官方文档的基础上增加了部分样例代码和示意图表,以帮助读者对文档的理 ...
- C++20 要来了!
867 人赞同了该文章 C++的新标准又双叒叕要到来了,是的,C++20要来了! 图片来源:udemy.com 几周前,C++标准委会历史上规模最大的一次会议(180人参会)在美国San Diego召 ...
- Python高级编程之生成器(Generator)与coroutine(一):Generator
转载请注明出处:点我 这是一系列的文章,会从基础开始一步步的介绍Python中的Generator以及coroutine(协程)(主要是介绍coroutine),并且详细的讲述了Python中coro ...
- skynet coroutine 运行笔记
阅读云大的博客以及网上关于 skynet 的文章,总是会谈服务与消息.不怎么看得懂代码,光读这些文字真的很空洞,不明白说啥.网络的力量是伟大的,相信总能找到一些解决自己疑惑的文章.然后找到了这篇讲解 ...
随机推荐
- git clone失败,超时,速度慢
最近使用git这个工具,发现git clone指令经常由于网络问题导致失败.查找相关资料之后,找到办法为修改网址,具体为: 将 git clone https://github.com/alibaba ...
- 惊爆!72.1K star 的 Netdata:实时监控与可视化的超炫神器!
在当今复杂的 IT 环境中,实时监控与可视化对于保障系统的稳定运行和性能优化至关重要. 无论是服务器.应用程序,还是网络设备,及时获取性能数据能够帮助我们快速定位问题.优化资源配置. Netdata, ...
- 给网站免费升级https协议
给网站免费升级HTTPS协议,可以通过申请并部署免费的SSL证书来实现.以下是一个详细的步骤指南: 一.申请免费SSL证书 选择证书颁发机构: 可以选择像JoySSL这样的公益项目,它提供免费.自动化 ...
- 云开发实践:从 0 到 1 带你玩 AI
今天我们将深入分析云开发的 AI 能力.这次的讨论焦点不再是之前提到的云端IDE编写代码的能力,而是更为广泛和实际的内容--如何利用云平台提供的各种模块化能力,快速高效地开发.今天的主题依然围绕AI展 ...
- P4629 SHOI2015 聚变反应炉
P4629 SHOI2015 聚变反应炉 树上背包+树形dp. 算是套娃题吗? 思路 看到数据考虑数据分治. part1 贪心 \(c_i\leq 1\) 对于这种情况,我们考虑贪心的点亮. 手玩几组 ...
- ScheduledThreadPoolExecutor与System#nanoTime
一直流传着Timer使用的是绝对时间,ScheduledThreadPoolExecutor使用的是相对时间,那么ScheduledThreadPoolExecutor是如何实现相对时间的? 先看看S ...
- AtCoder Beginner Contest 295
Three Days Ago 我们定义一个只由数字构成的字符串中的字符能够被重排成相同的两份,我们称这个字符串是个好字符串,比如12341234 现在给定一个字符串\(S\),找出所有的\([l,r] ...
- VLC web(http)控制 (4) 服务器文件获取
通过链接 http://127.0.0.1:8080/requests/browse.xml?uri=file%3A%2F%2F~ 可以获取服务器默认目录所有文件. 其中file%3A%2F%2F~是 ...
- 04. PART 2 IdentityServer4 ASP.NET Core Identity .NET Core 3.1
04. PART 2 IdentityServer4 ASP.NET Core Identity .NET Core 3.1 如果您已经来到这里,那么祝贺你的坚持,最难的部分已经完成了.我们仅仅需要的 ...
- 升级到 .NET Core 3.1
微软升级的频率有点快,转眼 .NET Core 升级到 3.1 版了,这是一个长期支持版本,意味着 .NET Core 正式进入成熟期. 不过,对于开发人员来说,你的项目又需要迁移了. 升级项目文件 ...