协程的原理(Coroutine Theory)

原文链接:https://lewissbaker.github.io/2017/09/25/coroutine-theory

This is the first of a series of posts on the C++ Coroutines TS, a new language feature that is currently on track for inclusion into the C++20 language standard.

这是C++ Cooutines TS系列文章中的第一篇,这是一种新的语言特性,目前正准备纳入C++20语言标准。

In this series I will cover how the underlying mechanics of C++ Coroutines work as well as show how they can be used to build useful higher-level abstractions such as those provided by the cppcoro library.

在本系列中,我将介绍C++Cooutines的底层机制是如何工作的,并演示如何使用它们构建有用的高级抽象,比如cppcoro库提供的抽象。

In this post I will describe the differences between functions and coroutines and provide a bit of theory about the operations they support. The aim of this post is introduce some foundational concepts that will help frame the way you think about C++ Coroutines.

在这篇文章中,我将描述函数和协程之间的差异,并提供一些关于它们所支持的操作的理论。这篇文章的目的是介绍一些基本概念,这些概念将有助于构建你关于C++协程的思考方式。

协程是函数,函数也是协程(Coroutines are Functions are Coroutines)

A coroutine is a generalisation of a function that allows the function to be suspended and then later resumed.

协程是一个函数的泛化,它允许函数被挂起,稍后再恢复。

I will explain what this means in a bit more detail, but before I do I want to first review how a “normal” C++ function works.

我将更详细地解释这意味着什么,但在此之前,我想先回顾一下“普通”C++函数是如何工作的。

“普通”函数(“Normal” Functions)

A normal function can be thought of as having two operations: Call and Return (Note that I’m lumping “throwing an exception” here broadly under the Return operation).

一个普通函数可以被认为有两个操作:调用返回(注意,我把“抛出一个异常”概括地放在了返回操作下面)。

The Call operation creates an activation frame, suspends execution of the calling function and transfers execution to the start of the function being called.

调用操作创建一个活跃帧,挂起调用函数的执行,并将执行转交到被调用函数的开始位置。

The Return operation passes the return-value to the caller, destroys the activation frame and then resumes execution of the caller just after the point at which it called the function.

返回操作将返回值传递给调用方,销毁活跃帧,然后在调用函数的位置恢复调用方的执行。

Let’s analyse these semantics a little more…

让我们对这些语义再多分析一点...

活跃帧(Activation Frames)

So what is this ‘activation frame’ thing?

那么什么是“活跃帧”呢?

You can think of the activation frame as the block of memory that holds the current state of a particular invocation of a function. This state includes the values of any parameters that were passed to it and the values of any local variables.

您可以将活跃帧看作是存储特定函数调用的当前状态的内存块。此状态包括传递给它的任何参数值和任何局部变量值。

For “normal” functions, the activation frame also includes the return-address - the address of the instruction to transfer execution to upon returning from the function - and the address of the activation frame for the invocation of the calling function. You can think of these pieces of information together as describing the ‘continuation’ of the function-call. ie. they describe which invocation of which function should continue executing at which point when this function completes.

对于“普通”函数,活跃帧还包括返回地址——从函数返回时要执行的指令的地址——以及调用函数的活跃帧的地址。您可以将这些信息一起看作是对函数调用的“继续执行”的描述。也就是说,它们描述了哪个函数的调用应该继续执行,何时该函数完成。

With “normal” functions, all activation frames have strictly nested lifetimes. This strict nesting allows use of a highly efficient memory allocation data-structure for allocating and freeing the activation frames for each of the function calls. This data-structure is commonly referred to as “the stack”.

对于“普通”函数,所有栈帧都具有严格嵌套的生命周期。这种严格的嵌套允许使用高效的内存分配数据结构,用于为每个函数调用分配和释放栈帧。这种数据结构通常被称为“栈”。

When an activation frame is allocated on this stack data structure it is often called a “stack frame”.

当在此栈数据结构上分配活跃帧时,通常称为“栈帧”。

This stack data-structure is so common that most (all?) CPU architectures have a dedicated register for holding a pointer to the top of the stack (eg. in X64 it is the rsp register).

这种栈数据结构非常常见,以至于大多数(全部?)CPU架构有一个专用寄存器,用于保存指向栈顶部的指针(例如。在X64中,它是rsp寄存器)。

To allocate space for a new activation frame, you just increment this register by the frame-size. To free space for an activation frame, you just decrement this register by the frame-size.

若要为新活跃帧分配空间,只需将此寄存器按帧大小递增即可。若要释放活跃帧的空间,只需将此寄存器按帧大小缩减。

“调用”操作(The ‘Call’ Operation)

When a function calls another function, the caller must first prepare itself for suspension.

当一个函数调用另一个函数时,调用方必须首先为挂起做好准备。

This ‘suspend’ step typically involves saving to memory any values that are currently held in CPU registers so that those values can later be restored if required when the function resumes execution. Depending on the calling convention of the function, the caller and callee may coordinate on who saves these register values, but you can still think of them as being performed as part of the Call operation.

这个“挂起”步骤通常包括将当前保存在CPU寄存器中的任何值保存到内存中,以便在函数恢复执行时,这些值可以在需要时恢复。根据函数的调用约定,调用方和被调用方可以协调谁保存这些寄存器值,但您仍然可以将它们视为调用操作的一部分。

The caller also stores the values of any parameters passed to the called function into the new activation frame where they can be accessed by the function.

调用方还将传递给被调用函数的任何参数的值存储到新的活跃帧中,在活跃帧中,函数可以访问这些参数。

Finally, the caller writes the address of the resumption-point of the caller to the new activation frame and transfers execution to the start of the called function.

最后,调用方将调用方恢复点的地址写入新的活跃帧,并将执行转交到被调用函数的开始位置。

In the X86/X64 architecture this final operation has its own instruction, the call instruction, that writes the address of the next instruction onto the stack, increments the stack register by the size of the address and then jumps to the address specified in the instruction’s operand.

在X86/X64体系结构中,这个最后的操作有自己的指令,即调用指令,它将下一个指令的地址写入栈,按地址的大小递增栈寄存器,然后跳转到指令的操作数中指定的地址。

“返回”操作(The ‘Return’ Operation)

When a function returns via a return-statement, the function first stores the return value (if any) where the caller can access it. This could either be in the caller’s activation frame or the function’s activation frame (the distinction can get a bit blurry for parameters and return values that cross the boundary between two activation frames).

当函数通过返回语句返回时,函数首先将返回值(如果有的话)存储在调用者可以访问它的地方。这可以是在调用方的活跃帧中,也可以是在函数的活跃帧中(对于跨越两个活跃怎之间边界的参数和返回值,这种区别可能会变得有点模糊)。

Then the function destroys the activation frame by:

  • Destroying any local variables in-scope at the return-point.
  • Destroying any parameter objects
  • Freeing memory used by the activation-frame

然后,该函数通过以下步骤销毁活跃帧:

  • 销毁返回点范围内的任何局部变量
  • 销毁任何参数对象
  • 释放活跃帧使用的内存

And finally, it resumes execution of the caller by:

  • Restoring the activation frame of the caller by setting the stack register to point to the activation frame of the caller and restoring any registers that might have been clobbered by the function.
  • Jumping to the resume-point of the caller that was stored during the ‘Call’ operation.

最后,它通过以下方式恢复调用者的执行:

  • 通过将栈寄存器设置为指向调用方的活跃帧,并恢复任何可能被该被调用函数破坏的寄存器,来恢复调用方的活跃帧。
  • 跳转到在“调用”操作期间存储的调用方的恢复点。

Note that as with the ‘Call’ operation, some calling conventions may split the repsonsibilities of the ‘Return’ operation across both the caller and callee function’s instructions.

请注意,与“调用”操作一样,一些调用约定可能会在调用方和被调用方函数的指令之间分割“返回”操作的责任。

协程(Coroutines)

Coroutines generalise the operations of a function by separating out some of the steps performed in the Call and Return operations into three extra operations: Suspend, Resume and Destroy.

协程泛化了函数的操作,将调用返回操作中执行的一些步骤划分为三个额外的操作:挂起恢复销毁

The Suspend operation suspends execution of the coroutine at the current point within the function and transfers execution back to the caller or resumer without destroying the activation frame. Any objects in-scope at the point of suspension remain alive after the coroutine execution is suspended.

挂起操作在函数的当前点挂起协程的执行,并在不破坏活跃帧的情况下将执行权转交给调用方或恢复调用方。在挂起协程执行之后,挂起点上的任何对象都仍然是可用的.

Note that, like the Return operation of a function, a coroutine can only be suspended from within the coroutine itself at well-defined suspend-points.

请注意,就像函数的返回操作一样,协程只能在携程内定义良好的挂起点上挂起。

The Resume operation resumes execution of a suspended coroutine at the point at which it was suspended. This reactivates the coroutine’s activation frame.

恢复操作将在挂起时恢复执行挂起的协程。这重新激活了协程的活跃帧。

The Destroy operation destroys the activation frame without resuming execution of the coroutine. Any objects that were in-scope at the suspend point will be destroyed. Memory used to store the activation frame is freed.

销毁操作销毁活跃帧而不恢复谢恒的执行。任何在挂起点范围内的对象都将被销毁,用于存储活跃帧的内存会被释放。

协程的活跃帧(Coroutine activation frames)

Since coroutines can be suspended without destroying the activation frame, we can no longer guarantee that activation frame lifetimes will be strictly nested. This means that activation frames cannot in general be allocated using a stack data-structure and so may need to be stored on the heap instead.

由于协同可以在不破坏激活帧的情况下被挂起,我们不能再保证活跃帧的生命周期内会被严格嵌套。这意味着活跃帧通常不能使用堆栈数据结构来分配,因此可能需要将其存储在堆中。

There are some provisions in the C++ Coroutines TS to allow the memory for the coroutine frame to be allocated from the activation frame of the caller if the compiler can prove that the lifetime of the coroutine is indeed strictly nested within the lifetime of the caller. This can avoid heap allocations in many cases provided you have a sufficiently smart compiler.

C++ Cooutines TS中,如果编译器能够证明协程的生命周期确实是在调用方的生命周期内严格嵌套的话,有一些规定允许从调用方的活跃帧中分配协程帧的内存。这在许多情况下可以避免堆分配,前提是您有足够聪明的编译器。

With coroutines there are some parts of the activation frame that need to be preserved across coroutine suspension and there are some parts that only need to be kept around while the coroutine is executing. For example, the lifetime of a variable with a scope that does not span any coroutine suspend-points can potentially be stored on the stack.

对于协程,活跃帧的某些部分需要在协程挂起时保存,而有些部分只需要在协程执行时保持。例如,具有不跨越任何协程挂起点的范围的变量的生命周期可以潜在地存储在栈上。

You can logically think of the activation frame of a coroutine as being comprised of two parts: the ‘coroutine frame’ and the ‘stack frame’.

您可以从逻辑上将协程的活跃帧看作是由两部分组成的:“协程帧”和“栈帧”。

The ‘coroutine frame’ holds part of the coroutine’s activation frame that persists while the coroutine is suspended and the ‘stack frame’ part only exists while the coroutine is executing and is freed when the coroutine suspends and transfers execution back to the caller/resumer.

“协程帧”持有协程的活跃帧的一部分,该活跃帧在协程被挂起时持续存在,而“栈帧”部分仅在协程执行时才存在,并在协程挂起并将执行转交回调用方/恢复调用方时释放。

“挂起”操作(The ‘Suspend’ operation)

The Suspend operation of a coroutine allows the coroutine to suspend execution in the middle of the function and transfer execution back to the caller or resumer of the coroutine.

协程的挂起操作允许协程在函数中间挂起执行,并将执行转交回协程的调用方或恢复调用方。

There are certain points within the body of a coroutine that are designated as suspend-points. In the C++ Coroutines TS, these suspend-points are identified by usages of the co_await or co_yield keywords.

在协程的主体中有一些被指定为挂起点的点。在C++ Coroutines TS中,这些挂起点是通过co_awaitco_yield关键字来标识的。

When a coroutine hits one of these suspend-points it first prepares the coroutine for resumption by:

  • Ensuring any values held in registers are written to the coroutine frame
  • Writing a value to the coroutine frame that indicates which suspend-point the coroutine is being suspended at. This allows a subsequent Resume operation to know where to resume execution of the coroutine or so a subsequent Destroy to know what values were in-scope and need to be destroyed.

当协程到达这些挂起点之一时,它首先通过以下方式为恢复协程做准备:

  • 确保将寄存器中保存的任何值写入协程帧
  • 将一个值写入协程帧,以指示在哪个位置挂起的协程。这允许后续的恢复操作知道在哪里恢复协程的执行,或者在后续的销毁时,知道哪些在范围内的对象需要被销毁。

Once the coroutine has been prepared for resumption, the coroutine is considered ‘suspended’.

一旦协程已经为恢复做好准备,该协同线被视为“挂起”。

The coroutine then has the opportunity to execute some additional logic before execution is transferred back to the caller/resumer. This additional logic is given access to a handle to the coroutine-frame that can be used to later resume or destroy it.

然后,协程有机会在执行转交回调用方/恢复调用方之前执行一些附加逻辑。这个附加逻辑被赋予对协程栈的句柄的访问权限,该句柄可用于以后恢复或销毁。

This ability to execute logic after the coroutine enters the ‘suspended’ state allows the coroutine to be scheduled for resumption without the need for synchronisation that would otherwise be required if the coroutine was scheduled for resumption prior to entering the ‘suspended’ state due to the potential for suspension and resumption of the coroutine to race. I’ll go into this in more detail in future posts.

这种在协程进入“挂起”状态后执行逻辑的能力允许将协程调度到恢复状态,而不需要同步,如果协程在进入“挂起”状态之前被调度执行恢复操作,则将需要同步,这是因为协程有可能挂起和恢复操作产生潜在的竞争。我将在以后的文章中更详细地讨论这个问题。

The coroutine can then choose to either immediately resume/continue execution of the coroutine or can choose to transfer execution back to the caller/resumer.

然后,协程可以选择立即恢复/继续执行协程,也可以选择将执行转交回调用方/恢复调用方。

If execution is transferred to the caller/resumer the stack-frame part of the coroutine’s activation frame is freed and popped off the stack.

如果将执行转交到调用方/恢复调用方,则释放协程活跃帧的栈帧部分,并将其从栈中弹出。

“恢复”操作(The ‘Resume’ operation)

The Resume operation can be performed on a coroutine that is currently in the ‘suspended’ state.

可以在当前处于“挂起”状态的协程上执行恢复操作。

When a function wants to resume a coroutine it needs to effectively ‘call’ into the middle of a particular invocation of the function. The way the resumer identifies the particular invocation to resume is by calling the void resume() method on the coroutine-frame handle provided to the corresponding Suspend operation.

当一个函数想要恢复一个协程时,它需要有效地“调用”到函数的特定调用过程中。恢复调用方标识要恢复的特定调用的函数,是调用相应挂起操作的协程帧句柄提供的void resume()方法。

Just like a normal function call, this call to resume() will allocate a new stack-frame and store the return-address of the caller in the stack-frame before transferring execution to the function.

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

However, instead of transferring execution to the start of the function it will transfer execution to the point in the function at which it was last suspended. It does this by loading the resume-point from the coroutine-frame and jumping to that point.

但是,它不是将执行转移到函数的开始,而是将执行转移到上次挂起的函数的点。它是通过从协程帧加载恢复点并跳到这一恢复点来实现的。

When the coroutine next suspends or runs to completion this call to resume() will return and resume execution of the calling function.

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

“销毁”操作(The ‘Destroy’ operation)

The Destroy operation destroys the coroutine frame without resuming execution of the coroutine.

销毁操作销毁协程帧,而不恢复协程的执行。

This operation can only be performed on a suspended coroutine.

此操作只能在挂起的协程上执行。

The Destroy operation acts much like the Resume operation in that it re-activates the coroutine’s activation frame, including allocating a new stack-frame and storing the return-address of the caller of the Destroy operation.

销毁操作与恢复操作非常相似,因为它重新激活了协程的活跃帧,包括分配新的栈帧和存储销毁操作调用方的返回地址。

However, instead of transferring execution to the coroutine body at the last suspend-point it instead transfers execution to an alternative code-path that calls the destructors of all local variables in-scope at the suspend-point before then freeing the memory used by the coroutine frame.

但是,它不是在最后一个挂起点将执行转交到协协程,而是将执行转交到另一个代码路径,该代码路径在挂起点调用范围内所有局部变量的析构函数,然后释放协程帧使用的内存。

Similar to the Resume operation, the Destroy operation identifies the particular activation-frame to destroy by calling the void destroy() method on the coroutine-frame handle provided during the corresponding Suspend operation.

恢复操作类似,该销毁操作通过在相应的挂起操作期间提供的协程帧句柄上调用void destroy()方法来标识要销毁的特定活跃帧。

协程的“调用”操作(The ‘Call’ operation of a coroutine)

The Call operation of a coroutine is much the same as the call operation of a normal function. In fact, from the perspective of the caller there is no difference.

协程的调用操作与普通函数的调用操作基本相同。事实上,从调用者的角度来看,没有什么不同。

However, rather than execution only returning to the caller when the function has run to completion, with a coroutine the call operation will instead resume execution of the caller when the coroutine reaches its first suspend-point.

但是,与函数调用在函数运行完毕时恢复调用方的执行不同,协程是在到达其第一个挂起点时恢复调用方的执行。

When performing the Call operation on a coroutine, the caller allocates a new stack-frame, writes the parameters to the stack-frame, writes the return-address to the stack-frame and transfers execution to the coroutine. This is exactly the same as calling a normal function.

在协程上执行调用操作时,调用方分配一个新的栈帧,将参数写入栈帧,将返回地址写入栈帧,并将执行转交到协程。这与调用普通函数完全相同。

The first thing the coroutine does is then allocate a coroutine-frame on the heap and copy/move the parameters from the stack-frame into the coroutine-frame so that the lifetime of the parameters extends beyond the first suspend-point.

协程所做的第一件事是在堆中分配一个协程帧,并将参数从栈帧复制/移动到协程帧,以便参数的生命周期超过第一个挂起点。

协程的“返回”操作(The ‘Return’ operation of a coroutine)

The Return operation of a coroutine is a little different from that of a normal function.

协程的返回操作与普通函数的返回操作略有不同。

When a coroutine executes a return-statement (co_return according to the TS) operation it stores the return-value somewhere (exactly where this is stored can be customised by the coroutine) and then destructs any in-scope local variables (but not parameters).

当协程执行返回语句(TS的co_return操作符)操作时,它会将返回值存储在某个地方(协程可以自定义这个值的存储位置),然后销毁任何作用域内的局部变量(而不是参数)。

The coroutine then has the opportunity to execute some additional logic before transferring execution back to the caller/resumer.

然后,协程有机会在将执行转交回调用方/恢复调用方之前执行一些附加逻辑。

This additional logic might perform some operation to publish the return value, or it might resume another coroutine that was waiting for the result. It’s completely customisable.

这个附加逻辑可能是执行一些操作来发布返回值,或者它可能恢复另一个等待结果的协程。完全可以自定义。

The coroutine then performs either a Suspend operation (keeping the coroutine-frame alive) or a Destroy operation (destroying the coroutine-frame).

然后,协同线执行挂起操作(保持协程帧是可用的)或销毁操作(销毁协程帧)。

Execution is then transferred back to the caller/resumer as per the Suspend/Destroy operation semantics, popping the stack-frame component of the activation-frame off the stack.

之后,按照挂起/销毁操作语义将执行转交回调用方/恢复调用方,从栈中弹出活跃帧的栈帧。

It is important to note that the return-value passed to the Return operation is not the same as the return-value returned from a Call operation as the return operation may be executed long after the caller resumed from the initial Call operation.

需要注意的是,传递给返回操作的返回值与从调用操作返回的返回值不相同,因为返回操作可能在调用方从初始调用操作恢复后很长时间之后才执行。

图解示例(An illustration)

To help put these concepts into pictures, I want to walk through a simple example of what happens when a coroutine is called, suspends and is later resumed.

为了帮助将这些概念更加形象的表示出来,我想介绍一个简单的例子,说明当一个协程被调用、挂起并在后面继续进行时会发生什么。

So let’s say we have a function (or coroutine), f() that calls a coroutine, x(int a).

Before the call we have a situation that looks a bit like this:

假设我们有一个函数(或协程),f(),它调用协程,x(Inta)。

在调用之前,我们现在的情况有点像这样:

STACK                     REGISTERS               HEAP

                          +------+
+---------------+ <------ | rsp  |
|  f()          |         +------+
+---------------+
| ...           |
|               |

Then when x(42) is called, it first creates a stack frame for x(), as with normal functions.

然后,当调用x(42)时,它首先为x()创建一个栈帧,就像普通函数一样。

STACK                     REGISTERS               HEAP
+----------------+ <-+
|  x()           |   |
| a  = 42        |   |
| ret= f()+0x123 |   |    +------+
+----------------+   +--- | rsp  |
|  f()           |        +------+
+----------------+
| ...            |
|                |

Then, once the coroutine x() has allocated memory for the coroutine frame on the heap and copied/moved parameter values into the coroutine frame we’ll end up with something that looks like the next diagram. Note that the compiler will typically hold the address of the coroutine frame in a separate register to the stack pointer (eg. MSVC stores this in the rbp register).Then, once the coroutine x() has allocated memory for the coroutine frame on the heap and copied/moved parameter values into the coroutine frame we’ll end up with something that looks like the next diagram. Note that the compiler will typically hold the address of the coroutine frame in a separate register to the stack pointer (eg. MSVC stores this in the rbp register).

然后,一旦协程x()为堆上的协程帧分配了内存,并将参数值复制/移动到协程帧中,我们将得到类似于下一个图的内容。注意,编译器通常会将协程帧的地址保存在栈指针的单独寄存器中(例如,MSVC将此存储在rbp寄存器中)。

STACK                     REGISTERS               HEAP
+----------------+ <-+
|  x()           |   |
| a  = 42        |   |                   +-->  +-----------+
| ret= f()+0x123 |   |    +------+       |     |  x()      |
+----------------+   +--- | rsp  |       |     | a =  42   |
|  f()           |        +------+       |     +-----------+
+----------------+        | rbp  | ------+
| ...            |        +------+
|                |

If the coroutine x() then calls another normal function g() it will look something like this.

如果协程x()又调用另一个普通函数g(),它将如下所示。

STACK                     REGISTERS               HEAP
+----------------+ <-+
|  g()           |   |
| ret= x()+0x45  |   |
+----------------+   |
|  x()           |   |
| coroframe      | --|-------------------+
| a  = 42        |   |                   +-->  +-----------+
| ret= f()+0x123 |   |    +------+             |  x()      |
+----------------+   +--- | rsp  |             | a =  42   |
|  f()           |        +------+             +-----------+
+----------------+        | rbp  |
| ...            |        +------+
|                |

When g() returns it will destroy its activation frame and restore x()’s activation frame. Let’s say we save g()’s return value in a local variable b which is stored in the coroutine frame.

当g()返回时,它将销毁其活跃帧,并恢复x()的活跃帧。假设我们将g()的返回值保存在一个局部变量b中,该变量存储在协程帧中。

STACK                     REGISTERS               HEAP
+----------------+ <-+
|  x()           |   |
| a  = 42        |   |                   +-->  +-----------+
| ret= f()+0x123 |   |    +------+       |     |  x()      |
+----------------+   +--- | rsp  |       |     | a =  42   |
|  f()           |        +------+       |     | b = 789   |
+----------------+        | rbp  | ------+     +-----------+
| ...            |        +------+
|                |

If x() now hits a suspend-point and suspends execution without destroying its activation frame then execution returns to f().

如果x()现在命中挂起点并在执行挂起后不销毁其活跃帧,则执行返回到f()。

This results in the stack-frame part of x() being popped off the stack while leaving the coroutine-frame on the heap. When the coroutine suspends for the first time, a return-value is returned to the caller. This return value often holds a handle to the coroutine-frame that suspended that can be used to later resume it. When x() suspends it also stores the address of the resumption-point of x() in the coroutine frame (call it RP for resume-point).

这将导致x()的栈帧部分从栈中弹出,同时将协程帧留在堆中。当协程第一次挂起时,返回值将返回给调用方。这个返回值通常包含一个句柄,该句柄被挂起,可用于以后恢复它。当x()挂起时,它也会将x()的恢复点的地址存储在协程帧中(RP表示恢复点)。

STACK                     REGISTERS               HEAP
                                        +----> +-----------+
                          +------+      |      |  x()      |
+----------------+ <----- | rsp  |      |      | a =  42   |
|  f()           |        +------+      |      | b = 789   |
| handle     ----|---+    | rbp  |      |      | RP=x()+99 |
| ...            |   |    +------+      |      +-----------+
|                |   |                  |
|                |   +------------------+

This handle may now be passed around as a normal value between functions. At some point later, potentially from a different call-stack or even on a different thread, something (say, h()) will decide to resume execution of that coroutine. For example, when an async I/O operation completes.

这个句柄现在可以作为函数之间的普通值传递。在以后的某个时候,可能来自不同的调用栈,甚至在不同的线程上,一些东西(例如,h())将决定继续执行该协程。例如,当异步I/O操作完成时。

The function that resumes the coroutine calls a void resume(handle) function to resume execution of the coroutine. To the caller, this looks just like any other normal call to a void-returning function with a single argument.

恢复协程的函数调用一个void resume(handle)函数来恢复协程的执行。对于调用者来说,这看起来就像对带单个参数的空返回值函数的任何其他普通调用一样。

This creates a new stack-frame that records the return-address of the caller to resume(), activates the coroutine-frame by loading its address into a register and resumes execution of x() at the resume-point stored in the coroutine-frame.

这将创建一个新的栈帧,该帧记录调用方的返回地址用来resume(),通过将其地址加载到寄存器中激活协程帧,并在存储在协程帧中的恢复点恢复x()的执行。

STACK                     REGISTERS               HEAP
+----------------+ <-+
|  x()           |   |                   +-->  +-----------+
| ret= h()+0x87  |   |    +------+       |     |  x()      |
+----------------+   +--- | rsp  |       |     | a =  42   |
|  h()           |        +------+       |     | b = 789   |
| handle         |        | rbp  | ------+     +-----------+
+----------------+        +------+
| ...            |
|                |

总结(In summary)

I have described coroutines as being a generalisation of a function that has three additional operations - ‘Suspend’, ‘Resume’ and ‘Destroy’ - in addition to the ‘Call’ and ‘Return’ operations provided by “normal” functions.

除了“普通”函数提供的“调用”和“返回”操作之外,我还将协程描述为一个函数的泛化,该函数有三个附加操作——“暂停”、“恢复”和“销毁”。

I hope that this provides some useful mental framing for how to think of coroutines and their control-flow.

我希望这能为如何思考协程及其控制流提供一些有用的思维框架。

In the next post I will go through the mechanics of the C++ Coroutines TS language extensions and explain how the compiler translates code that you write into coroutines.

在下一篇文章中,我将介绍C++ Cooutines TS语言扩展的机制,并解释编译器如何将您编写的代码转换为协程。

posted @ 2020-02-23 16:51  寒带鱼  阅读(2281)  评论(1编辑  收藏  举报