1. 程式人生 > >協程的原理(Coroutine Theory)

協程的原理(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_await或co_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語言擴充套件的機制,並解釋編譯器如何將您編寫的程式碼轉換為協程