原文鏈接: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語言擴展的機制,並解釋編譯器如何將您編寫的代碼轉換為協程。