全国网站制作公司排名,网站建设幽默,wordpress 招聘插件,怎么使用wordpress原文
了解对称转移
协程组提供了个编写异步代码的绝妙方法,与同步代码一样.只需要在合适地点加上协待,编译器就会负责挂起协程,跨挂起点保留状态,并在操作完成后恢复协程.
但是,最初有个令人讨厌的限制,如果不小心,很容易导致栈溢出.如果想避免它,则必须引入额外同步成本,以…原文
了解对称转移
协程组提供了个编写异步代码的绝妙方法,与同步代码一样.只需要在合适地点加上协待,编译器就会负责挂起协程,跨挂起点保留状态,并在操作完成后恢复协程.
但是,最初有个令人讨厌的限制,如果不小心,很容易导致栈溢出.如果想避免它,则必须引入额外同步成本,以便在任务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){}标::协柄任务::承诺类型协程_;
};
任务::等待器 任务::符号 协待()无异{中 等待器{协程_};
}从而完成任务类型必要代码.
栈溢出问题
但是,当你在协程中开始写循环,且协待可在该循环体中同步完成的任务时,就会出现实现限制. 如:
任务 同步完成(){协中;
}
任务 同步循环(整 数){对(整 i0;i数;i){协待 同步完成();}
}上述简单任务实现,计数为10,1000甚至100000时,同步循环()函数(可能)正常工作.但是,可能会传递一个值(如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;
}使用原始实现,保证在协待 福()之后运行的代码,在完成福()的同一线程上内联运行. 如,一个可能的输出是:
条11234
福11234
福23456
条23456然而,随着使用原子,完成福()可能会与挂起条()竞争,因此表明,有时协待 福()之后的代码可能会,在条()开始执行的原始线程上运行. 如,现在可如下输出:
条11234
福11234
福23456
条21234对许多用例,该行为可能不会有影响.但是,对旨在转换执行环境的算法,会有问题. 如,通过()算法等待一些可等待,然后在指定分发的执行环境中生成它.此算法的简化版本如下:
元型名 可等待,型名 调度器
任务等待结果型可等待通过(可等待 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.承诺().连续;
}现在有个既没有空返回挂起协风味所有的栈溢出问题,也没有布尔返回挂起协风味的不确定性恢复环境问题的任务实现.
可视化栈
现在再看看原始示例:
任务 同步完成(){协中;
}
任务 同步循环(整 数){对(整 i0;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,如果需要恢复另一个协程,请使用对称转移风格.
添加到C20的协程中的新对称转移功能使得不必担心栈溢出的,编写递归相互恢复协程更加容易.此功能是创建高效且安全的异步协程类型(如任务)的关键.