原文
介绍
设置场景
定义任务类型
1
:确定承诺
类型
2
:创建协程状态
第3步:调用取中()
4
:初挂起点
5
:记录挂起点
第6步:实现协柄::恢复()
和协柄::消灭()
第7步:实现协柄<承诺>::承诺()
和从承诺()
8
:协程体的开始
9
:降级协待
式
10
:实现对异常()
11
:实现协中
12
:实现终挂起()
13
:实现对称转移
和无操协程
(地址)
最后,绑定在一起
"了解C++
协程"系列的先前博客讨论了编译器对协程及其协待,协产
和协中
式等不同类型的转换.
协程理论
C++
协程:了解协待
符号这里
C++
协程:了解承诺类型
C++
协程:了解对称转移
首先,假设有个既为可等待
返回类型,又为协程返回
类型的基本任务
类型.为了简单,假设此协程
类型允许异步
生成整
.
本文,介绍如何降级
以下协程
函数到不包含协程协待/协中
关键字的C++
代码中,以便更好理解.
//其他函数的前向声明.实现不重要.
任务 f(整 x);
//转换为非`C++`代码的简单协程
任务 g(整 x){
整 fx=协待 f(x);
协中 fx*fx;
}
首先,声明要用的任务类
.
为了理解协程
是如何降级的,不想知道该类型
的方法的定义.降级
会插入调用它.
类 任务{
公:
构 等待器;
类 承诺类型{
公:
承诺类型()无异;
~承诺类型();
构 止等待器{
极 直接协()无异;
标::协柄<>挂起协(
标::协柄<承诺类型>h)无异;
空 恢复协()无异;
};
任务 取中()无异;
标::总是挂起 初挂起()无异;
止等待器 终挂起()无异;
空 对异常()无异;
空 中值(整 结果)无异;
私:
友 任务::等待器;
标::协柄<>连续_;
标::变量<标::单态,整,标::异常针>结果_;
};
任务(任务&&t)无异;
~任务();
任务&符号=(任务&&t)无异;
构 等待器{
显 等待器(标::协柄<承诺类型>h)无异;
极 直接协()无异;
标::协柄<承诺类型>挂起协(
标::协柄<>h)无异;
整 恢复协();
私:
标::协柄<承诺类型>协程_;
};
等待器 符号 协待()&&无异;
私:
显 任务(标::协柄<承诺类型>h)无异;
标::协柄<承诺类型>协程_;
};
1
:确定承诺类型任务 g(整 x){
整 fx=协待 f(x);
协中 fx*fx;
}
编译器发现此函数包含(协待,协产
或协中)
三个之一的协程
关键字时,它启动协程转换
.
第一步
是确定此协程要用的承诺类型
.
这是按标::协征
的模板参数替换返回
类型和参数类型的签名
来确定的.
如,对有任务返回类型
和单个整
类型的参数的g
函数,编译器使用标::协征<任务,整>::承诺类型
查找它.
定义别名
,以便稍后可引用它:
用__g承诺型=标::协征<任务,整>::承诺类型;
注意:用前双下划线
来指示是编译器生成的内部符号
,不应在用户代码中使用它.
现在,因为没有特化标::协征
,会实例化仅按返回类型
的嵌套承诺类型
名的别名
定义嵌套承诺类型
的主模板.
即,示例中,应该按任务::承诺类型
类型解析.
2
:创建协程状态协程
函数需要在挂起
时保留协程,参数和局部变量
的状态,以便在恢复
协程时仍可用.
标准C++
中,此状态叫协程状态
,一般由堆分配.
首先为g
协程的协程状态
定义一个结构.
还不知道其内容,暂时留空.
构__g状态{
//待填弃
};
协程状态
包含许多不同的内容:
1,承诺
对象
2,函数参数
副本
3,有关协程当前挂起的挂起点
及恢复/析构
信息
4,生命期
跨挂起点的局部/临时
变量的存储
首先为承诺
对象和参数
副本添加存储.
构__g状态{
整 x;
__g_promise_t__承诺;
//待填充
};
接着,应添加一个构造器
来初化
这些数据成员.
(如果该调用有效)编译器试先用参数副本
的左值引用
来调用承诺
构造器,否则回退
到调用承诺
类型的默认构造器.
创建简单助手
来帮助解决该问题:
元<型名 承诺,型名...形参>
承诺 构造承诺([[也许未用]]形参&...形参){
如 常式(标::可构造从<承诺,形参&...>){
中 承诺(形参...);
}异{
中 承诺();
}
}
因此,协程状态
构造器可能如下:
构__g状态{
__g状态(整&&x)
:x(静转<整&&>(x))
,__承诺(构造承诺<__g承诺型>(x))
{}
整 x;
__g_promise_t__承诺;
//待填充
};
现在已有了表示协程状态
类型的开始,也开始实现g()
的降级,方法是给它传递函数参数
,并堆分配__g状态
类型实例,以便可复制/移动
它们到协程状态
.
"斜坡函数"指代协程
实现的包含初化
协程状态并准备好开始执行协程
逻辑的一部分
.即,它是进入执行协程体
的一个入口
.
任务 g(整 x){
动*状态=新__g状态(静转<整&&>(x));
//...实现其余斜坡功能
}
注意,承诺
类型未自定义新符号
,所以在此只是调用全局::符号 新
.
如果承诺
类型确实自定义了新
符号,则它调用新
符号而不是全局::符号 新
.首先检查,是否可用(大小,参数值…)参数列表调用新
符号.
如果是
,则用该参数列表
调用它.否则,仅用(大小)
参数列表来调用它.新
符号访问协程
函数的参数列表
的功能有时叫"参数预览
",想为协程状态
用按参数传递
的分配器
分配存储
时非常有用.
如果编译器发现__g承诺型::符号 新
定义,则降级为以下逻辑:
元<型名 承诺,型名...实参>
空*__承诺分配(标::大小型 大小,[[也许未用]]实参&...实参){
如 常式(要求{承诺::符号 新(大小,实参...);}){
中 承诺::符号 新(大小,实参...);
}异{
中 承诺::符号 新(大小);
}
}
任务 g(整 x){
空*状态内存=__承诺分配<__g承诺型>(的大小(__g状态),x);
__g状态*状态;
试{
状态=::新(状态内存)__g状态(静转<整&&>(x));
}抓(...){
__g承诺型::符号 删(状态内存);
抛;
}
//...实现其余的斜坡功能
}
此外,此承诺
类型不定义分配失败上取中对象()
静态成员函数.如果此函数是在承诺
类型上定义的,则这里的分配
应改用标::不抛型
形式的新
符号,且在返回空针
时中 __g承诺型::分配失败上取中对象();
.
即像这样:
任务 g(整 x){
动*状态=::新(标::不抛)__g状态(静转<整&&>(x));
如(状态==空针){
中__g承诺型::分配失败上取中对象();
}
//...实现其余的斜坡功能
}
为简单起见,后面只使用最简单
的调用全局::符号 新
的内存分配函数.
get_return_object()
斜坡函数做的下一件事是在承诺
对象上调用取中()
方法来取斜坡
函数的返回值
.
按局部变量
存储返回值,并在斜坡
函数结束时(在其他步骤完成后)返回.
任务 g(整 x){
动*状态=新__g状态(静转<整&&>(x));
推导(动)中值=状态->__承诺.取中();
//...实现其余斜坡功能
中 中值;
}
但是,现在调用取中()
可能会抛,此时,要释放分配的协程状态
.因此,为了平衡,给状态的所有权加上标::独针
,以便后续
触发异常
时释放它:
任务 g(整 x){
标::独针<__g状态>状态(新__g状态(静转<整&&>(x)));
推导(动)中值=状态->__承诺.取中();
//...实现其余斜坡功能
中 中值;
}
4
:初挂起点在调用取中()
后,斜坡
函数要做的下一件事是开始执行协程体
,而协程体中的第一件事是初始
挂起点.
即求值协待 承诺.初挂起()
.
现在,最好,只按初挂起
对待协程,然后按恢复
初挂起的协程实现
启动协程的.但是,初始
挂起点的规范在如何处理异常
和协程状态
生命期方面有一些怪癖.
这是在C++20
发布之前对初挂起点语义的后期调整,以修复此处的一些相关问题.
在计算
初挂起点中,如果从以下位置
触发异常:
1,调用初挂起()
,
2,在(如果定义了)调用返回的可等待
的协待()
符号,
3,等待器上调用直接协()
,或
4,等待器上调用挂起协()
则,异常
传播回斜坡
函数的调用者,并自动析构
协程状态.
如果从以下位置触发异常:
1,调用恢复协()
,
2,从(如果可以)协待()
符号返回的对象析构器,或
3,从初挂起()
返回的对象析构器
则,此协程体抓异常
并调用承诺.对异常()
.
表明要小心
处理这部分转换
,因为一部分需要在斜坡
函数中,而另一部分需要在协程体
中.
此外,因为从初挂起()
和(可选)协待()
符号返回的对象
有跨挂起点的生命期
(在挂起协程
点前创建,并在恢复协程
后析构),因此要在协程状态
中存储
这些对象.
此例,从初挂起()
返回的类型是恰好是个空的,可平凡构造
的标::总是挂起
简单类型.
但是,逻辑上讲,仍要在协程状态
下存储该类型实例,因此为其添加存储
,以展示工作原理.
只会在调用初挂起()
这里构造该对象,所以要添加允许显式
控制生命期的数据成员
.
为此,先定义个人工生命期
助手类,它是三元可构造的,也是可平凡析构
的,但在需要时允许
显式构造/析构
存储在那里的值.
元<型名 T>
构 人工生命期{
人工生命期()无异=默认;
~人工生命期()=默认;
//不可复制/移动
人工生命期(常 人工生命期&)=删;
人工生命期(人工生命期&&)=删;
人工生命期&符号=(常 人工生命期&)=删;
人工生命期&符号=(人工生命期&&)=删;
元<型名 工厂>
要求
标::可调用<工厂&>&&
标::相同于<标::调用结果型<工厂&>,T>
T&构造从(工厂 工厂)无异(标::是可不抛调用值<工厂&>){
中*::新(静转<空*>(&存储))T(工厂());
}
空 消灭()无异(标::是可析构不抛值<T>){
标::消灭在(标::加载器(重转<T*>(&存储)));
}
T&取()&无异{
中*标::加载器(重转<T*>(&存储));
}
私:
对齐为(T)标::字节 存储[的大小(T)];
};
注意,构造从()
方法设计为带λ
,而不是带构造器参数
.这在用函数
调用结果初化
变量时,允许利用有保证
的复制省略来原位
构造对象.
如果带构造器参数
,则最终会不必要地调用
额外移动构造器
.
现在可用此人工生命期
结构为承诺.初挂起()
返回的临时数据成员
声明数据成员
.
构__g状态{
__g状态(整&&x);
整 x;
__g_promise_t__承诺;
人工生命期<标::总是挂起>__1临;
//待填充
};
标::总是挂起
类型没有协待()
符号,因此不必为此处调用的结果
保留额外临时空间
.
一旦调用初挂起()
构造了该对象,需要调用三个
方法来实现协待
式:直接协(),挂起协()
和恢复协()
.
调用挂起协()
时,要给它传递
当前协程的句柄.现在可调用标::协柄<__g承诺型>::从承诺()
并传递该承诺
的引用.
此外,调用.挂起协(句柄)
的结果有空
类型,因此与对布尔
值和返回协柄
的风格那样,不必考虑调用挂起协()
后是恢复此协程
还是另一个协程
.
最后,因为标::总是挂起
等待器上的所有方法调用
都按无异
声明,因此无需担心异常
.如果它们可能抛,则需要添加额外
代码,来确保在异常
传播出斜坡
函数前析构临时标::总是挂起
对象.
一旦挂起协()
成功返回或可开始执行协程体
时,就可以抛异常
时不再需要自动析构
协程状态.
因此,可在拥有协程状态
的标::独针
上调用释放()
以避免从函数
返回时析构它.
现在可实现初挂起
式的第一部分,如下:
任务 g(整 x){
标::独针<__g状态>状态(新__g状态(静转<整&&>(x)));
推导(动)中值=状态->__承诺.取中();
状态->__1临.构造从([&]()->推导(动){
中 状态->__承诺.初挂起();
});
如(!状态->__1临.取().直接协()){
//...此处挂起协程
状态->__1临.取().挂起协(
标::协柄<__g承诺型>::从承诺(状态->__承诺));
状态.释放();
//落入下面的返回语句.
}异{
//没有挂起`协程`.
状态.释放();
//...开始执行协程体
}
中__中值;
}
在协程体中而不是在斜坡
函数中,调用恢复协()
和__1临
的析构器.
现在求值
初挂起点,(大部分)可工作,但是在该斜坡
函数的代码中仍有几个待办
.为了解决它,不忙,先看看挂起
协程并稍后恢复
协程的策略
.
5
:记录挂起点挂起协程
时,要确保在控制流中的挂起点
恢复它.
还要跟踪
在每个挂起点
,有自动存储持续时间
对象是否失活
,以便知道如果析构而不是恢复协程
,则要析构
哪些对象.
可为协程
中的每个挂起点
赋值一个唯一编号
,然后在协程状态
的整数
数据成员中存储
它.
然后,每当挂起协程
时,都会在协程状态的挂起点编号
成员中写入编号,恢复/析构
时,检查此整数
以查看在哪个
挂起点挂起.
注意,这不是在协程状态
下存储挂起点
的唯一方法,但是所有3个主要编译器(微软,c语言,g编)
都使用此方法.
另一个潜在方法是为每个
挂起点使用单独的恢复/消灭
函数指针.
因此,用整数
数据成员存储
挂起点索引
并初化为零(0作为初始值)来扩展协程状态.
构__g状态{
__g状态(整&&x);
整 x;
__g_promise_t__承诺;
整__挂起点=0;//<--添加挂起点索引
人工生命期<标::总是挂起>__1临;
//待填充
};
coroutine_handle::resume()
和coroutine_handle::destroy()
调用协柄::恢复()
恢复协程时,需要最终调用一些实现挂起协程体
其余部分的函数.然后,调用体函数可查找挂起点索引
并跳转到控制流
中的相应点.
还要实现协柄::消灭()
函数,以便它调用适当逻辑
来析构当前挂起点
处的域内
对象,且要实现协柄::完成()
来查询当前挂起点
是否是最终
挂起点.
协柄
方法的接口不知道具体的协程状态
类型,协柄<空>
类型可指向任一协程实例
.表明需要类型擦除
协程状态类型
.
为此,可存储该协程类型的恢复/消灭
函数的函数指针,并让协柄::恢复/消灭()
调用这些函数指针
.
协柄
类型还需要可使用协柄::地址()
和协柄::从地址()
方法,来转换为空*
或从空*
转换.
此外,可从任一协程句柄
恢复/析构协程,而不仅是传递给最新挂起协()
调用的句柄.
因此要定义协柄
类型,以便它只包含协程状态
指针,且按协程状态的数据成员
而不是在协柄
中,存储恢复/消灭
函数指针.
此外,因为需要协柄
来指向任意协程状态
对象,因此需要在所有协程状态
类型中,函数指针
数据成员的布局
保持一致.
一个直接方法是让每个
协程状态类型从包含这些数据成员的某个基类继承
.
如,可按所有协程状态
类型基类
定义以下类型
构__协程状态{
用__恢复函数=空(__协程状态*);
用__消灭函数=空(__协程状态*);
__恢复函数*__恢复;
__消灭函数*__消灭;
};
然后协柄::恢复()
方法可传递__协程状态
对象指针
简单调用__恢复()
.
类似,可为协柄::消灭()
方法和__消灭
函数指针这样.
对协柄::完成()
方法,选择以挂起无效
的__恢复
函数指针为最终点
.这很方便,因为最终挂起点不支持恢复()
,只支持消灭()
.
如果有人在最终
挂起点(有未定义行为)挂起
的协程上调用恢复()
,则最终会调用很快就会失败并指出他们错误的空
函数指针.
因此,可如下实现协柄<空>
类型:
名间 标
{
元<型名 承诺=空>
类 协柄;
元<>
类 协柄<空>{
公:
协柄()无异=默认;
协柄(常 协柄&)无异=默认;
协柄&符号=(常 协柄&)无异=默认;
空*地址()常{
中 静转<空*>(状态);
}
静 协柄 从地址(空*针){
协柄 h;
h.状态=静转<__协程状态*>(针);
中 h;
}
显 符号 极()无异{
中 状态!=空针;
}
友 极 符号==(协柄 a,协柄 b)无异{
中 a.状态==b.状态;
}
空 恢复()常{
状态->__恢复(状态);
}
空 消灭()常{
状态->__消灭(状态);
}
极 完成()常{
中 状态->__恢复==空针;
}
私:
__协程状态*状态=空针;
};
}
coroutine_handle::promise()
和from_promise()
对更通用的协柄<承诺>
特化,大多数实现都可重用协柄<空>
实现.但是,还需要可访问从承诺()
方法返回的协程状态
的承诺
对象,并从承诺
对象引用构建
一个协柄
.
但是,不能简单
指向具体
协程状态类型,因为协柄<承诺>
类型必须可引用承诺
类型是承诺
的协程状态
.
要定义个新的从__协程状态
继承并包含承诺
对象的协程状态基类
,以便可定义使用指定承诺
类型,并从该基类
继承的所有协程状态类型
.
元<型名 承诺>
构__带承诺协程状态:__协程状态{
__带承诺协程状态()无异{}
~__带承诺协程状态(){}
联{
承诺__承诺;
};
};
为什么在此匿名联
内声明__承诺
成员?
原因是,为指定协程
函数创建的继承类
包含参数复制
数据成员的定义
.默认,在基类
的数据成员之后初化
继承类中的数据成员,因此按普通
数据成员声明承诺
对象,表明它是在参数
复制数据成员前构造的.
但是,要在参数副本构造器之后调用承诺
的构造器,可能需要把参数副本的引用
传递给承诺
构造器.
因此,在此基类中为承诺
对象保留存储,以便它与协程状态
的开头有一致的偏移,但让继承类
负责在初化参数副本
后适当位置调用构造器/析构器
.
按联声明__承诺
成员可满足要求.
更新__g状态
类,以便现在从该新的基类
继承.
构__g状态:__带承诺协程状态<__g承诺型>{
__g状态(整&&__x)
:x(静转<整&&>(__x)){
//使用`原位新`在基类中初化`承诺`对象
::新((空*)标::的地址(本->__承诺))
__g承诺型(构造承诺<__g承诺型>(x));
}
~__g状态(){
//还需要在`析构`参数对象前,手动调用`承诺`析构器.
本->__承诺.~__g承诺型();
}
整__挂起点=0;
整 x;
人工生命期<标::总是挂起>__1临;
//待填充
};
现在已定义了承诺
基类,现在可实现标::协柄<承诺>
类模板.
除了使用__带承诺协程状态<承诺>
指针而不是__协程状态
指针外,大多数实现应该与协柄<空>
中的等效方法基本相同.
唯一新东西是添加了承诺()
和从承诺()
函数.
1,承诺()
方法很直接,它只返回协程状态的__承诺
成员的引用.
2,从承诺()
方法要求从承诺
对象的地址计算协程状态
地址.可从承诺
对象的地址中减去__承诺
成员的偏移
来完成.
实现协柄<承诺>
:
名间 标
{
元<型名 承诺>
类 协柄{
用 状态型=__带承诺协程状态<承诺>;
公:
协柄()无异=默认;
协柄(常 协柄&)无异=默认;
协柄&符号=(常 协柄&)无异=默认;
符号 协柄<空>()常 无异{
中 协柄<空>::从地址(地址());
}
显 符号 极()常 无异{
中 状态!=空针;
}
友 极 符号==(协柄 a,协柄 b)无异{
中 a.状态==b.状态;
}
空*地址()常{
中 静转<空*>(静转<__协程状态*>(状态));
}
静 协柄 从地址(空*针){
协柄 h;
h.状态=静转<状态型*>(静转<__协程状态*>(针));
中 h;
}
承诺&承诺()常{
中 状态->__承诺;
}
静 协柄 从承诺(承诺&承诺){
协柄 h;
//知道`__承诺`成员的地址,因此通过从该地址中减去`__承诺`字段的偏移来计算协程状态的地址.
h.状态=重转<状态型*>(
重转<正 符*>(标::的地址(承诺))-
的偏移(状态型,__承诺));
中 h;
}
//根据`"协柄<空>"`实现来定义它们
空 恢复()常{
静转<协柄<空>>(*本).恢复();
}
空 消灭()常{
静转<协柄<空>>(*本).消灭();
}
极 完成()常{
中 静转<协柄<空>>(*本).完成();
}
私:
状态型*状态;
};
}
现在已定义了恢复
协程机制,现在可返回到的"斜坡"
函数,并更新它以初化添加到协程状态
的新函数指针
数据成员.
8
:协程体的开始现在前向声明正确签名的恢复/消灭
函数,并更新__g状态
构造器以初化协程状态
,以便恢复/析构器
指针指向它们:
空__g恢复(__协程状态*s);
空__g消灭(__协程状态*s);
构__g状态:__带承诺协程状态<__g承诺型>{
__g状态(整&&__x)
:x(静转<整&&>(__x)){
//初化`协柄`方法使用的函数指针.
本->__恢复=&__g恢复;
本->__消灭=&__g消灭;
//用原位新`初化`基类中的`承诺`对象
::新((空*)标::的地址(本->__承诺))
__g承诺型(构造承诺<__g承诺型>(x));
}
//其余部分省略
};
任务 g(整 x){
标::独针<__g状态>状态(新__g状态(静转<整&&>(x)));
推导(动)中值=状态->__承诺.取中();
状态->__1临.构造从([&]()->推导(动){
中 状态->__承诺.初挂起();
});
如(!状态->__1临.取().直接协()){
状态->__1临.取().挂起协(
标::协柄<__g承诺型>::从承诺(状态->__承诺));
状态.释放();
//落入下面的返回语句.
}异{
//未挂起`协程`.立即开始执行协程体.
__g恢复(状态.释放());
}
中 中值;
}
完成了斜坡
函数,现在可专注g()
的恢复/析构器
.
从完成初挂起
式的降级开始.
调用__g恢复()
,且__挂起点
索引为0时,需要在__1临
上调用恢复协()
然后调用__1临
的析构器来恢复.
空__g恢复(__协程状态*s){
//`"s"`指向`__g状态`.
动*状态=静转<__g状态*>(s);
//生成跳转表,来根据`挂起点索引`的值跳转到代码中的正确位置.
开关(状态->__挂起点){
若 0:至 挂起点_0;
默认:标::不可达();
}
挂起点_0:
状态->__1临.取().恢复协();
状态->__1临.消灭();
//`待办`:实现协程体的其余部分.`整 fx=协待 f(x);协中 fx*fx;`
}
调用__g消灭()
且__挂起点
索引为0时,要先析构__1临
,然后再析构并释放
协程状态.
空__g消灭(__协程状态*s){
动*状态=静转<__g状态*>(s);
开关(状态->__挂起点){
若 0:至 挂起点_0;
默认:标::不可达();
}
挂起点_0:
状态->__1临.消灭();
至 消灭状态;
//待办:在此为其他`挂起点`添加额外逻辑.
消灭状态:
删 状态;
}
9
:降级co_await
式接着,看看降级协待 f(x)
式.
首先,要计算返回临时任务对象
的f(x)
.
因为直到语句尾的分号
才会析构临时任务
,且语句包含协待
式,因此任务的生命期跨一个挂起点
,因此必须在协程状态中存储任务
.
在此临时任务上计算协待
式时,要调用返回临时等待器
对象的协待()
符号方法.此对象的生命期也跨挂起点
,因此必须以协程状态
存储它.
添加必要
成员到__g状态
类型中:
构__g状态:__带承诺协程状态<__g承诺型>{
__g状态(整&&__x);
~__g状态();
整__挂起点=0;
整 x;
人工生命期<标::总是挂起>__1临;
人工生命期<任务>__2临;
人工生命期<任务::等待器>__3临;
};
然后可更新__g恢复()
函数来初化这些临时函数,然后计算构成协待
式其余部分的3个直接协,挂起协
和恢复协
调用.
注意,任务::等待器::挂起协()
方法返回一个协程句柄
,因此需要生成恢复
返回句柄的代码.
还要在调用挂起协()
前更新挂起点
索引(挂起对此点使用1索引
),然后在跳转表
中添加个额外的项
以确保恢复
到正确位置.
空__g恢复(__协程状态*s){
//知道`"s"`指向`__g状态`.
动*状态=静转<__g状态*>(s);
//生成跳转表,根据`挂起点`索引值跳转到代码中的正确位置.
开关(状态->__挂起点){
若 0:至 挂起点_0;
若 1:至 挂起点_1;//<--添加新的跳转表项
默认:标::不可达();
}
挂起点_0:
状态->__1临.取().恢复协();
状态->__1临.消灭();
//`整 fx=协待 f(x);`
状态->__2临.构造从([&]{
中 f(状态->x);
});
状态->__3临.构造从([&]{
中 静转<任务&&>(状态->__2临.取()).符号 协待();
});
如(!状态->__3临.取().直接协()){
//标记挂起点
状态->__挂起点=1;
动 h=状态->__3临.取().挂起协(
标::协柄<__g承诺型>::从承诺(状态->__承诺));
//返回前`恢复`返回的协程句柄.
h.恢复();
中;
}
挂起点_1:
整 fx=状态->__3临.取().恢复协();
状态->__3临.消灭();
状态->__2临.消灭();
//待办:实现`协中 fx*fx;`
}
注意,整 fx
局部变量的生命期
不跨挂起点,因此不需要以协程状态
存储它.可在__g恢复
函数中按普通
局部变量存储它.
还要向__g消灭()
函数添加,在此挂起点何时析构
协程的必要项.
空__g消灭(__协程状态*s){
动*状态=静转<__g状态*>(s);
开关(状态->__挂起点){
若 0:至 挂起点_0;
若 1:至 挂起点_1;//<--添加新的跳转表项
默认:标::不可达();
}
挂起点_0:
状态->__1临.消灭();
至 消灭状态;
挂起点_1:
状态->__3临.消灭();
状态->__2临.消灭();
至 消灭状态;
//待办:在此为其他挂起点添加额外逻辑.
消灭状态:
删 状态;
}
所以现在已完成了语句的实现:
整 fx=协待 f(x);
但是,未按无异
标记f(x)
函数,因此可能会触发异常.此外,等待器::恢复协()
方法也没有按无异
标记,因此也可能触发
异常.
从协程体
抛异常时,编译器会生成代码
来抓异常,然后调用承诺.对异常()
让承诺处理异常.看看实现.
10
:实现unhandled_exception()
规范说,协程行为类似:
{
承诺-类型 承诺 承诺-构造器-实参;
试{
协待 承诺.初挂起();
函数-体
}抓(...){
如(!初始-等待-恢复-调用)
抛;
承诺.对异常();
}
止-挂起:
协待 承诺.终挂起();
}
已在斜坡
函数中单独处理了初始恢复协
调用的分支,因此在此无需担心.
调整__g恢复()
函数以在主体
周围插入试/抓
块.
注意,需要小心地在试块
内,放置跳转到正确位置
的开关
,因为禁止使用至
进入试块
.
此外,要小心,在试/抓
块之外从挂起协()
返回的协程句柄
上调用.恢复()
.如果在返回的协程上调用.恢复()
触发异常
,则当前
协程不应抓该异常,而应从恢复
此协程的恢复()
调用中传播
出去.
因此,在函数顶部声明的变量
中存储
协程句柄,然后至
跳转到试/抓
外的地方,并在那里调用.恢复()
.
空__g恢复(__协程状态*s){
动*状态=静转<__g状态*>(s);
标::协柄<空>待恢复协程;
试{
开关(状态->__挂起点){
若 0:至 挂起点_0;
若 1:至 挂起点_1;//<--添加新的跳转表项
默认:标::不可达();
}
挂起点_0:
状态->__1临.取().恢复协();
状态->__1临.消灭();
//整 fx=协待 f(x);
状态->__2临.构造从([&]{
中 f(状态->x);
});
状态->__3临.构造从([&]{
中 静转<任务&&>(状态->__2临.取()).符号 协待();
});
如(!状态->__3临.取().直接协()){
状态->__挂起点=1;
待恢复协程=状态->__3临.取().挂起协(
标::协柄<__g承诺型>::从承诺(状态->__承诺));
至 恢复协程;
}
挂起点_1:
整 fx=状态->__3临.取().恢复协();
状态->__3临.消灭();
状态->__2临.消灭();
//待办:实现`协中 fx*fx;`
}抓(...){
状态->__承诺.对异常();
至 终挂起;
}
终挂起:
//待办:实现`协待 承诺.终挂起();`
恢复协程:
待恢复协程.恢复();
中;
}
但是,上面的代码中有个错误.如果带异常退出__3临.取().恢复协()
调用,在抓到异常
前无法调用__3临
和__2临
的析构器.
注意,不能在此简单
抓异常,调用析构器
并重抛异常,因为如果这些析构器
调用标::未处理异常()
,因为会处理异常
,这会改变这些析构器
的行为.
但是,如果在展开异常
时析构器调用
此函数,则调用标:::未处理异常()
应返回非零值
.
可改为定义资取化
助手类,来确保触发
异常时在出域
时调用析构器
.
元<型名 T>
构 析构器警卫{
显 析构器警卫(人工生命期<T>&对象)无异
:针_(标::的地址(对象))
{}
//不可移动
析构器警卫(析构器警卫&&)=删;
析构器警卫&符号=(析构器警卫&&)=删;
~析构器警卫()无异(标::是可析构不抛值<T>){
如(针_!=空针){
针_->消灭();
}
}
空 取消()无异{针_=空针;}
私:
人工生命期<T>*针_;
};
//不需要调用析构器的类型的部分特化.
元<型名 T>
要求 标::是可平凡析构值<T>
构 析构器警卫<T>{
显 析构器警卫(人工生命期<T>&)无异{}
空 取消()无异{}
};
//推导类模板参数以简化使用
元<型名 T>
析构器警卫(人工生命期<T>&对象)->析构器警卫<T>;
使用此工具,现在可在抛异常
时,用此类型
来确保析构存储
在协程状态中的变量.
还可用此类
来调用现有变量
的析构器,以便在它们自然出域
时也调用它们的析构器.
空__g恢复(__协程状态*s){
动*状态=静转<__g状态*>(s);
标::协柄<空>待恢复协程;
试{
开关(状态->__挂起点){
若 0:至 挂起点_0;
若 1:至 挂起点_1;//<--添加新的跳转表项
默认:标::不可达();
}
挂起点_0:
{
析构器警卫 临1析构器{状态->__1临};
状态->__1临.取().恢复协();
}
//整 fx=协待 f(x);
{
状态->__2临.构造从([&]{
中 f(状态->x);
});
析构器警卫 临2析构器{状态->__2临};
状态->__3临.构造从([&]{
中 静转<任务&&>(状态->__2临.取()).符号 协待();
});
析构器警卫 临3析构器{状态->__3临};
如(!状态->__3临.取().直接协()){
状态->__挂起点=1;
待恢复协程=状态->__3临.取().挂起协(
标::协柄<__g承诺型>::从承诺(状态->__承诺));
//不退出域的挂起`协程`.所以取消析构器守卫.
临3析构器.取消();
临2析构器.取消();
至 恢复协程;
}
//不要在此退出域.不能"跳转到"进入带`非平凡析构器`变量域的标签.因此,在此必须不调用`析构器`,退出`析构器`保护的域,然后在`"挂起点_1"`标签后重建它们.
临3析构器.取消();
临2析构器.取消();
}
挂起点_1:
整 fx=[&]()->推导(动){
析构器警卫 临2析构器{状态->__2临};
析构器警卫 临3析构器{状态->__3临};
中 状态->__3临.取().恢复协();
}();
//待办:实现`协中 fx*fx;`
}抓(...){
状态->__承诺.对异常();
至 终挂起;
}
终挂起:
//待办:实现`协待 承诺.终挂起();`
//
恢复协程:
待恢复协程.恢复();
中;
}
现在,协程体
在有异常
时正确析构
局部变量,如果这些异常
从协程体传播出去,会正确调用承诺.对异常()
.
注意,也要特殊处理承诺.对异常()
方法自身带异常
退出的情况(如,如果它重抛
当前异常).
此时,协程要抓异常
,在最终挂起点
按挂起标记
协程,然后重新触发
异常.
如:__g恢复()
函数的抓块
如下:
试{
//...
}抓(...){
试{
状态->__承诺.对异常();
}抓(...){
状态->__挂起点=2;
状态->__恢复=空针;//按最终挂起点标记
抛;
}
}
还要在__g消灭
函数的跳转表
中添加个额外项
:
开关(状态->__挂起点){
若 0:至 挂起点_0;
若 1:至 挂起点_1;
若 2:至 消灭状态;//域中没有需要析构的变量,只需析构`协程状态`对象.
}
注意,此时,最终挂起点不一定与协待 承诺.终挂起()
的挂起点相同.
这是因为承诺.终挂起()
挂起点一般会有一些调用协柄::消灭()
时,与协待
式关联的需要析构的额外
临时对象.
然而,如果承诺.对异常()
带异常退出,则会析构
这些临时对象,因此不需要由协柄::消灭()
析构它.
11
:实现co_return
下一步是实现协中 fx*fx;
语句.
与前面步骤相比,这相对简单.
映射协中<式>
语句为:
承诺.中值(<式>);
至 止-挂起-点;
因此,可简单地替换待办
注解为:
状态->__承诺.中值(fx*fx);
至 终挂起;
容易.
12
:实现final_suspend()
代码中的最后待办
现在是实现协待 承诺.终挂起()
语句.
终挂起()
方法返回要在协程状态中存储
,并在__g消灭
中析构的临时任务::承诺类型::止等待器
类型.
它没有自己的协待()
符号,因此不必用额外临时对象
来取调用结果.
与任务::等待器
类型一样,它也使用挂起协()
协程句柄返回形式
.所以在返回的句柄
上要确保调用恢复()
.
如果未在最终挂起点挂起协程
,则隐式析构
协程状态.因此,如果执行到达协程尾
,就需要删除状态
对象.
此外,因为所有最终挂起
逻辑都要求为无异
,因此无需担心此处的子式
会触发异常.
先添加数据成员到__g状态
类型.
构__g状态:__带承诺协程状态<__g承诺型>{
__g状态(整&&__x);
~__g状态();
整__挂起点=0;
整 x;
人工生命期<标::总是挂起>__1临;
人工生命期<任务>__2临;
人工生命期<任务::等待器>__3临;
人工生命期<任务::承诺类型::止等待器>__4临;//<---
};
然后可实现止-挂起
式的主体,如下:
终挂起:
//协待 承诺.终挂起
{
状态->__4临.构造从([&]()无异{
中 状态->__承诺.终挂起();
});
析构器警卫 临4析构器{状态->__4临};
如(!状态->__4临.取().直接协()){
状态->__挂起点=2;
状态->__恢复=空针;//按最终挂起点标记
待恢复协程=状态->__4临.取().挂起协(
标::协柄<__g承诺型>::从承诺(状态->__承诺));
临4析构器.取消();
至 恢复协程;
}
状态->__4临.取().恢复协();
}
//如果执行到达协程尾,则析构协程状态
删 状态;
中;
现在还需要更新__g消灭
函数来处理该新的挂起点.
空__g消灭(__协程状态*状态){
动*状态=静转<__g状态*>(s);
开关(状态->__挂起点){
若 0:至 挂起点_0;
若 1:至 挂起点_1;
若 2:至 挂起点_2;
默认:标::不可达();
}
挂起点_0:
状态->__1临.消灭();
至 消灭状态;
挂起点_1:
状态->__3临.消灭();
状态->__2临.消灭();
至 消灭状态;
挂起点_2:
状态->__4临.消灭();
至 消灭状态;
消灭状态:
删 状态;
}
现在有个全功能的g()
协程函数的降级.
大功告成!就是这样!
13
:实现对称转移和无操协程
(地址)表明,上述__g恢复()
函数的实现方式有问题.见前面
[式.等待]
规范提示了应该如何处理挂起协
的协程句柄
返回风格:
如果等待-挂起
的类型为标::协程_-句柄
,则计算等待-挂起.恢复()
.
注:这恢复等待挂起
结果引用的协程.可这样连续
恢复多个协程,最终控制流
返回到当前协程
调用者或恢复程序.
注解虽非规范
,因此无约束,但强烈鼓励编译器
如下实现:实现尾调用
以恢复下个
协程,而不是递归
恢复下个协程.
这是因为如果在循环中相互恢复协程
,则递归
恢复下个协程很容易导致无限栈增长
.
问题是在在__g恢复()
函数的主体中的下个协程
上调用.恢复()
,然后返回,因此在下个
协程挂起并返回后才会释放__g恢复()
帧使用的栈空间
.
编译器可按尾调用
恢复下个协程
.这样,编译器生成的代码
首先弹出当前栈帧
,保留
返回地址,然后跳转到下个协程
的恢复函数.
因为C++
不能指定按尾调用调用函数
,因此需要实际
从恢复
函数返回,以便可释放
其栈空间,然后让调用者恢复
下个协程.
因为在挂起
时,下个
协程可能会无限地,需要恢复
另一个协程,因此调用者需要在循环
中恢复
协程.
此循环一般叫"蹦床循环
",因为从一个协程
返回到循环
,然后从循环"反弹
"回下个协程
.
如果修改恢复
函数签名以返回下个协程
的协程状态的指针,而不是返回空
,则协柄::恢复()
函数可立即调用__恢复()
函数指针,让下个协程恢复它.
更改__协程状态
的__恢复函数
的签名:
构__协程状态{
用__恢复函数=__协程状态*(__协程状态*);
用__消灭函数=空(__协程状态*);
__恢复函数*__恢复;
__消灭函数*__消灭;
};
然后可如下编写协柄::恢复()
函数:
空 标::协柄<空>::恢复()常{
__协程状态*s=状态;
干{
s=s->__恢复(s);
}当(/*一些条件*/);
}
然后问题变成了:“条件
应该是什么”
因此引入标::无操协程()
助手.
标::无操协程()
是个返回特殊的有无操作
的恢复()
和消灭()
方法的协程句柄
的工厂函数.
如果挂起协程
并从挂起协()
方法返回无操作协程句柄
,则表明没有更多
要恢复的协程,且恢复此协程的协程句柄::恢复()
的调用应返回给调用者.
因此,要实现标::无操协程()
和协柄::恢复()
中的条件,以便条件返回假
,且当__协程状态
指针指向无操协程状态
时退出循环
.
在此可定义一个按无操
协程状态指定的__协程状态
的静态实例.标::无操协程()
函数可返回该对象
协程句柄,可比较__协程状态
指针与该对象
地址,以查看指定
协程句柄是否是无操协程
.
首先,定义该特殊的无操协程状态
对象:
构__协程状态{
用__恢复函数=__协程状态*(__协程状态*);
用__消灭函数=空(__协程状态*);
__恢复函数*__恢复;
__消灭函数*__消灭;
静__协程状态*__无操恢复(__协程状态*状态)无异{
中 状态;
}
静 空__无操消灭(__协程状态*)无异{}
静 常__coroutine_state__无操协程;
};
内联 常__coroutine_state__coroutine_state::__无操协程{
&__协程状态::__无操恢复,
&__协程状态::__无操消灭
};
然后可特化标::协柄<无操协程承诺>
.
名间 标
{
构 无操协程承诺{};
用 无操协程句柄=协柄<无操协程承诺>;
无操协程句柄 无操协程()无异;
元<>
类 协柄<无操协程承诺>{
公:
常式 协柄(常 协柄&)无异=默认;
常式 协柄&符号=(常 协柄&)无异=默认;
常式 显 符号 极()无异{中 真;}
常式 友 极 符号==(协柄,协柄)无异{
中 真;
}
符号 协柄<空>()常 无异{
中 协柄<空>::从地址(地址());
}
无操协程承诺&承诺()常 无异{
静 无操协程承诺 承诺;
中 承诺;
}
常式 空 恢复()常 无异{}
常式 空 消灭()常 无异{}
常式 极 完成()常 无异{中 假;}
常式 空*地址()常 无异{
中 常转<__协程状态*>(&__协程状态::__无操协程);
}
私:
常式 协柄()无异=默认;
友 无操协程句柄 无操协程()无异{
中{};
}
};
}
可更新协柄::恢复()
以在返回无操协程状态
时退出.
空 标::协柄<空>::恢复()常{
__协程状态*s=状态;
干{
s=s->__恢复(s);
}当(s!=&__协程状态::__无操协程);
}
最后,可更新__g恢复()
函数,现在返回__协程状态*
.
这仅涉及更新签名
及:
//用
动 h=...;
中 静转<__协程状态*>(h.地址());
//替换
待恢复协程=...;
至 恢复协程;
然后在函数
的最后(在删除 状态;
语句之后)添加
中 静转<__协程状态*>(标::无操协程().地址());
协程状态
类型的__g状态
比它需要的更大.
4个临时值的数据成员各自保留了相应存储
.但是,某些临时值
生命期不会重叠,因此从理论上讲,可在对象
生命期结束后为下个对象
重用对象存储来节省协程状态
空间.
为了利用它,可在适当时在匿名联
中定义数据成员.
查看拥有的临时变量的生命期:
1,__1临
仅在协待 承诺.初挂起();
语句中
2,__2临
仅在整 fx=协待 f(x)
中
3,__3临
仅在嵌套在__2临
的生命期内的整 fx=协待 f(x)
中
4,__4临
仅在协待 承诺.终挂起()
中.
因为__2临
和__3临
的生命期重叠,必须把它们放在一个结构
中,因为它们需要同时存在.
但是,__1临
和__4临
成员没有重叠
生命期,因此可把它们放在一个匿名联
中.
因此,可更改数据成员为:
构__g状态:__带承诺协程状态<__g承诺型>{
__g状态(整&&x);
~__g状态();
整__挂起点=0;
整 x;
构__1域{
人工生命期<任务>__2临;
人工生命期<任务::等待器>__3临;
};
联{
人工生命期<标::总是挂起>__1临;
__1域__s1;
人工生命期<任务::承诺类型::止等待器>__4临;
};
};
然后,因为__2临
和__3临
变量,现在嵌套在__s1
对象中,要更新它们引用,如状态->__s1.2临
.但除此外
,其余代码
不变.
这应该可节省额外的16
个字节的协程状态
大小,因为不再需要额外
存储__1临
和__4临
数据成员,否则尽管是空类型,也会被填充
到指针大小.
最后绑定在一起
好的,所以为协程
函数生成的最终代码
:
任务 g(整 x){
整 fx=协待 f(x);
协中 fx*fx;
}
如下:
//协程承诺类型
用__g承诺型=标::协征<任务,整>::承诺类型;
__协程状态*__g恢复(__协程状态*s);
空__g消灭(__协程状态*s);
//协程状态定义
构__g状态:__带承诺协程状态<__g承诺型>{
__g状态(整&&x)
:x(静转<整&&>(x)){
//初化`协柄`方法使用的函数指针.
本->__恢复=&__g恢复;
本->__消灭=&__g消灭;
//在初化参数副本后,使用`原位新`初化基类中的`承诺`对象.
::新((空*)标::的地址(本->__承诺))
__g承诺型(构造承诺<__g承诺型>(本->x));
}
~__g状态(){
本->__承诺.~__g承诺型();
}
整__挂起点=0;
//参数副本
整 x;
//局部变量/临时变量
构__1域{
人工生命期<任务>__2临;
人工生命期<任务::等待器>__3临;
};
联{
人工生命期<标::总是挂起>__1临;
__1域__s1;
人工生命期<任务::承诺类型::止等待器>__4临;
};
};
//"斜坡"函数
任务 g(整 x){
标::独针<__g状态>状态(新__g状态(静转<整&&>(x)));
推导(动)中值=状态->__承诺.取中();
状态->__1临.构造从([&]()->推导(动){
中 状态->__承诺.初挂起();
});
如(!状态->__1临.取().直接协()){
状态->__1临.取().挂起协(
标::协柄<__g承诺型>::从承诺(状态->__承诺));
状态.释放();
//落入下面的返回语句.
}异{
//没有挂起`协程`.立即开始执行协程体.
__g恢复(状态.释放());
}
中 中值;
}
//"恢复"函数
__协程状态*__g恢复(__协程状态*s){
动*状态=静转<__g状态*>(s);
试{
开关(状态->__挂起点){
若 0:至 挂起点_0;
若 1:至 挂起点_1;//<--添加新的跳转表项
默认:标::不可达();
}
挂起点_0:
{
析构器警卫 临1析构器{状态->__1临};
状态->__1临.取().恢复协();
}
//整 fx=协待 f(x);
{
状态->__s1.__2临.构造从([&]{
中 f(状态->x);
});
析构器警卫 临2析构器{状态->__s1.__2临};
状态->__s1.__3临.构造从([&]{
中 静转<任务&&>(状态->__s1.__2临.取()).符号 协待();
});
析构器警卫 临3析构器{状态->__s1.__3临};
如(!状态->__s1.__3临.取().直接协()){
状态->__挂起点=1;
动 h=状态->__s1.__3临.取().挂起协(
标::协柄<__g承诺型>::从承诺(状态->__承诺));
//不退出域挂起协程.所以取消析构器守卫.
临3析构器.取消();
临2析构器.取消();
中 静转<__协程状态*>(h.地址());
}
//不要在此退出域.不能"跳转到"进入带`非平凡析构器`变量域的标签.因此,在此必须不调用`析构器`,退出`析构器`保护的域,然后在`"挂起点_1"`标签后重建它们.
临3析构器.取消();
临2析构器.取消();
}
挂起点_1:
整 fx=[&]()->推导(动){
析构器警卫 临2析构器{状态->__s1.__2临};
析构器警卫 临3析构器{状态->__s1.__3临};
中 状态->__s1.__3临.取().恢复协();
}();
//`协中 fx*fx;`
状态->__承诺.中值(fx*fx);
至 终挂起;
}抓(...){
状态->__承诺.对异常();
至 终挂起;
}
终挂起:
//`协待 承诺.终挂起`
{
状态->__4临.构造从([&]()无异{
中 状态->__承诺.终挂起();
});
析构器警卫 临4析构器{状态->__4临};
如(!状态->__4临.取().直接协()){
状态->__挂起点=2;
状态->__恢复=空针;//按最终挂起点标记
动 h=状态->__4临.取().挂起协(
标::协柄<__g承诺型>::从承诺(状态->__承诺));
临4析构器.取消();
中 静转<__协程状态*>(h.地址());
}
状态->__4临.取().恢复协();
}
//如果执行到协程尾,则析构协程状态
删 状态;
中 静转<__协程状态*>(标::无操协程().地址());
}
//"析构"函数
空__g消灭(__协程状态*s){
动*状态=静转<__g状态*>(s);
开关(状态->__挂起点){
若 0:至 挂起点_0;
若 1:至 挂起点_1;
若 2:至 挂起点_2;
默认:标::不可达();
}
挂起点_0:
状态->__1临.消灭();
至 消灭状态;
挂起点_1:
状态->__s1.__3临.消灭();
状态->__s1.__2临.消灭();
至 消灭状态;
挂起点_2:
状态->__4临.消灭();
至 消灭状态;
消灭状态:
删 状态;
}