主頁 > 後端開發 > 協程的原理(Coroutine Theory)

協程的原理(Coroutine Theory)

2020-09-16 20:52:11 後端開發

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

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/59714.html

標籤:C++

上一篇:演算法訓練 旅行家的預算

下一篇:c++輸入輸出,保留幾位小數

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more