0%

现代c++协程第一篇

协程(Coroutine)既是函数也是协程

协程:允许暂停的函数(函数的泛化)

普通函数的工作原理

普通函数包含两个操作 : call && return

  • call: 暂停函数执行,创建 activation frame 转到被调用函数的执行
  • return: 将返回值保存,传给调用者,销毁 activation frame,在调用处恢复调用者的执行

Activation Frames

the block of memory that holds the current state of a particular invocation of a function

保存了函数调用状态的内存,包括参数的值和局部变量的值

对于普通函数, activation frame 保存返回地址(通常由 call 指令保存),

以及调用者的 activation frame 的地址,以便恢复该函数的执行状态

普通函数的 activation frame 具有严格的 lifetimes 嵌套,这允许高效的数据结构来分配和释放,比如 stack

所以 activation frame 也通常称为 stack frame

Coroutines

协程通过将 Call 和 Return 操作中执行的一些步骤分离为三个额外操作来概括函数的操作:

SuspendResumeDestroy

  • Suspend 暂停当前协程执行,并将执行转移回调用方或恢复方,协程执行暂停后,暂停时作用域内的任何对象仍保持存活状态。

  • Resume 在挂起的协程的挂起点处恢复执行。

  • Destroy 销毁 Coroutine activation frame 不再恢复执行。挂起点范围内的任何对象都将被销毁。

Coroutine activation frames

  • 由于协程可以在不销毁激活帧的情况下挂起,因此我们不能再保证激活帧的生存期将严格嵌套。激活帧可能需要存储在堆上。

  • C++ 协程规定,如果编译器可以证明协程的生命周期确实严格嵌套在调用方的生命周期内,则允许从调用方的激活帧分配协程帧的内存。

  • 对于协程,激活帧的某些部分需要在协程挂起期间保留,而有些部分只需要在协程执行时保留。例如一个 scope 不跨越挂起点的变量

  • Coroutine 逻辑上分为:(coroutine frame)协程帧和(stack frame)栈帧

  • “协程帧”保存协程激活帧的部分,该部分在挂起时持续存在,“栈帧”部分仅在协程执行时存在,并在协程挂起并转移回调用者或恢复者时释放

The Suspend operation

  • 挂起操作使得协程在函数中间暂停执行,并将执行转移回协程的调用者或恢复者。

  • 挂起点通过使用 co_awaitco_yield 关键字来标识。

  • 到达挂起点时,要准备恢复的数据,准备工作:寄存器中保存的值写入协程帧,写入暂存点。准备好了,即可视为suspended

  • 协程可以在执行转移回调用者/恢复者之前执行一些附加逻辑。该附加逻辑可以访问协程帧的句柄,该句柄可用于稍后恢复或销毁

  • 挂起后执行逻辑的能力允许恢复协程,或者转移到调用者或恢复者

  • 如果执行转移到调用者/恢复者,则协程激活帧的栈帧部分将被释放并从栈中弹出。

The Resume operation

  • 恢复操作可以在处于suspended状态的协程上执行。

  • 当函数想要恢复协程时,是通过调用被挂起的协程帧句柄上的 void resume() 方法。

  • 像普通的函数调用一样,对 resume() 的调用分配一个新的栈帧,并在将执行转移之前将调用者的返回地址存储在栈帧中。

  • 但是,它不会将执行转移到函数的开头,而是将执行转移到函数中上次挂起的位置。

  • 当协程下次挂起或运行对 resume()调用完成时,将返回并恢复调用函数的执行。

The Destroy operation

  • Destroy 操作会销毁协程帧,此操作只能在挂起的协程上执行。
  • 但是,它不是将执行转移到一个代码块,该代码块在挂起点调用作用域内所有局部变量的析构函数,然后释放协程帧使用的内存。
  • Destroy 操作通过调用相应的 Suspend状态的协程帧句柄上的 void destroy() 方法来销毁的特定激活帧。

The Call operation of a coroutine

  • 协程的调用与普通函数的调用相似,从调用者的角度来看没有什么区别

  • 然而,当函数运行完成时,执行不会仅返回到调用者,而对于协程,调用操作将在协程到达其第一个挂起点时恢复调用者的执行。

  • 协程执行 Call 时,调用者分配一个新的栈帧,保存参数和返回地址到栈帧,并专业将执行转移到协程。

  • 协程的第一件事是在堆上分配一个协程帧,并复制/移动参数从栈帧到协程帧中

The Return operation of a coroutine

  • 协程的 Return 与普通函数的 Return 有所不同。
  • 当协程执行 co_return 时,它保存返回值(具体由由协程定义),然后销毁范围内的局部变量(除了参数)
  • 在将执行转移回调用者/恢复者之前执行一些附加逻辑。
  • 此附加逻辑可能会执行某些操作来发布返回值,或者可能会恢复另一个正在等待结果的协程。
  • 然后,协程执行Suspend/Destroy
  • 然后根据**Suspend/Destroy ** 的语义将执行转移回调用者/恢复者,将激活帧的堆栈帧组件从堆栈中弹出。
  • 注意:传递给 co_return的返回值与从 Call 的返回值不同,因为返回操作可能会从初始 Call 恢复之后很长时间才执行。

Coroutine summery

将协程描述为函数的概括,除了“正常”函数提供的‘Call’ and ‘Return’之外,该函数还具有三个附加操作——‘Suspend’, ‘Resume’和‘Destroy