C++協程:理解promise類型

本文是我們翻譯的C++協程相關的第三篇文章,原作者是Lewis Baker。原文請參見:https://lewissbaker.github.io/2018/09/05/understanding-the-promise-type。以下為正文。

這篇文章是C++協程技術規範(N4736)相關係列文章中的第三篇。

這個系列之前的文章包括:

  • 協程理論
  • 理解co_await運算符

在本文中,我會探討編譯器將你編寫的協程代碼轉換為編譯後代碼的機制,以及你如何通過定義自己的Promise類型來自定義協程的行為。

協程的概念

協程技術規範添加了三個新關鍵字:co_await、co_yield和co_return。在一個函數體(body of a function)中,無論使用這三個關鍵字的哪一個,都會觸發編譯器將函數作為協程而不是普通函數進行編譯。【譯註:根據我們的理解,我們認為原文中「body」一詞有兩種含義:一個是指由我們寫的原始代碼語句構成的函數方法體(包括編譯器對co_await等的翻譯),另一個是由編譯器根據promise控制邏輯轉換後的代碼構成的協程方法體。為明確起見,我們分別使用函數體協程體來指代它們。】

編譯器會以一種非常機械的方式將你編寫的代碼轉換為一個狀態機,它會讓函數在一些特定點上掛起執行,然後在稍後恢復執行。

在上一篇文章中,我描述了技術規範引入的兩個新介面中的第一個:Awaitable介面。而技術規範引入的第二個介面就是Promise介面,這個介面對於上述代碼轉換至關重要。

Promise的介面方法用於自定義協程本身的行為。通過此介面,庫開發者能夠自定義協程被調用時會發生什麼,協程返回(正常返回或發生異常)時會發生什麼。還可以自定義協程內每一個co_await或co_yield表達式的行為。

Promise對象

通過實現一組在協程執行期間特定點上被調用的方法,Promise對象就能定義和控制協程自身的行為。

在我們繼續之前,我希望你能夠試著擺脫對什麼是promise的任何先入為主的觀念。儘管在一些使用場景中,協程的promise對象確實與跟std::future配對使用的std::promise起到了相似的作用。但在其他場景中,這個類比就顯得有些牽強。可能將協程的promise對象看作是一個協程狀態控制器會更容易理解,它控制協程的行為,以及被用於跟蹤協程的狀態。

對一個協程函數的每一次調用,都會在協程幀中構造一個promise的對象實例。

編譯器會負責生成在協程執行期間關鍵點上對promise對象特定方法的調用代碼。

在以下示例中,假定為一次特定的協程調用而在協程幀中創建的promise對象實例變數為promise

當你寫的一個函數體(由語句<body-statements>構成)中包含了協程關鍵字(co_return、co_await、co_yield)之一時,該函數體大體上會被轉化為如下的代碼:【譯註:以下注釋由我們「擅自」添加,移除所有注釋就是原文。】


當一個協程函數被調用時,在正式執行函數體代碼之前會先執行幾個額外的步驟,這與常規的函數略有不同。

以下是對這些步驟的概述(我將在下文對每一個步驟進行詳細說明)。

  1. 使用new運算符分配一個協程幀(可選步驟)。
  2. 將所有的函數參數複製到協程幀。
  3. 調用promise對象的構造函數,該對象的類型為P。
  4. 調用promise.get_return_object()方法獲取一個結果,此結果將在協程首次掛起時返回給調用方。將此結果保存到一個局部變數中。
  5. 調用promise.initial_suspend()方法,並使用co_await等待返回結果。
  6. 當co_await promise.initial_suspend()表達式恢復執行時(立即恢復或非同步恢復),開始執行你寫的函數體代碼語句。

在執行你的語句過程中,如果遇到一個co_return,那麼會執行如下幾個額外的步驟:

  1. 調用promise.return_void()或promise.return_value(<expr>)。【譯註:取決於你的協程方法向調用方最終返回值的類型以及promise的實現方式。比如一個非同步網路發送可能不會返回值,而非同步網路接收可能會向調用方返回讀取到的位元組數。】
  2. 銷毀具有自動存儲生命周期的所有變數,銷毀順序與它們被創建的順序相反。
  3. 調用promise.final_suspend()方法,並使用co_await等待返回結果。

如果你的<body-statements>沒有正常co_return,而是拋出了未處理的異常,那麼:

  1. 捕獲該異常,並在catch塊中調用promise.unhandled_exception()。
  2. 調用promise.final_suspend()方法,並使用co_await等待返回結果。

一旦執行到協程體之外,協程幀就被銷毀。這通過如下的幾個步驟完成:

  1. 調用promise對象的析構函數。
  2. 調用所有函數參數副本的析構函數。
  3. 使用delete運算符釋放協程幀佔用的內存(可選步驟)。
  4. 將執行返回給調用方或恢復程序。

當執行首次到達一個co_await表達式內的<return-to-caller-or-resumer>時,或者協程沒有進入<return-to-caller-or-resumer>就執行結束時,協程將被掛起或銷毀,先前從promise.get_return_object()返回的對象會被進一步返回給協程的調用方。

為協程幀分配內存

首先,編譯器會生成對new運算符的調用代碼,該代碼負責為協程幀分配內存。

如果promise類型P上有一個自定義的new運算符重載,那麼調用此運算符重載,否則調用全局的new運算符。

需要特別注意的幾點是:

傳遞給new運算符的尺寸並不是sizeof(P),而是整個協程幀的大小。這個大小是編譯器根據如下需求自動計算得到的,具體包括:參數的個數和尺寸,promise對象的尺寸,局部變數的個數和尺寸,以及其他為管理協程狀態而特定於編譯器的存儲需求。

為了優化代碼,在滿足如下條件時,編譯器可能會省略對new運算符的調用:

  • 確定協程幀的生命周期會嚴格嵌套在調用方的生命周期內;並且
  • 編譯器能夠在調用點處確定協程幀所需要的尺寸。

滿足這些條件時,編譯器就可以在調用方的活動幀(可能是堆棧幀部分,也可能是協程幀部分)中為協程幀分配存儲空間。

技術規範還沒有明確在哪些情況下可以確保不必進行存儲分配工作,因此你仍然需要編寫代碼,以應對在為協程幀分配內存過程中可能產生的std::bad_alloc異常。這也意味著,你通常不應當將協程函數聲明為noexcept,除非你的程序允許在為協程幀分配內存失敗時調用std::terminate()【譯註:如果我們將協程函數聲明為noexcept,那麼當為協程幀分配內存失敗時,由於這個分配工作是編譯器生成的代碼完成的,而在我們自己代碼的控制範圍之外,那麼就會導致noexcept規則被違反,此時C++運行時就會調用std::terminate()】。

不過,我們有一個後備方案,可用來應對為協程幀分配內存失敗的情況,而不是(眼睜睜看著它)拋出異常。這在不允許異常發生的運行環境中可能是必要的,比如嵌入式環境中,或不能容忍異常所帶來的開銷的高性能環境中。

如果我們為promise類型提供了一個靜態的P::get_return_object_on_allocation_failure()成員函數,那麼編譯器就會生成new(size_t, nothrow_t)重載的調用代碼,而不是默認的new。如果此調用返回了nullptr,那麼協程將立即調用P::get_return_object_on_allocation_failure(),並將此調用結果返回給協程的調用方,而不是拋出異常。

自定義協程幀的內存分配

你的promise類型可以提供一個new()運算符的重載,當編譯器需要為使用了你的promise類型的協程幀分配內存時,就會調用你的重載版本而不是全局的new運算符。

例如:


「可是,自定義內存分配器不行嗎?」,我聽到你如此問道。

你仍然可以提供一個攜帶額外參數的P::operator new()重載,在匹配的情況下該重載會被調用,並將協程函數參數的左值引用傳遞給這些額外的參數。這可以讓一個內存分配器作為協程函數的參數傳遞給new運算符,然後new運算符就可以調用該內存分配器的allocate()方法。

不過,你需要做一些額外的工作,以便在分配的內存中包含內存分配器的一個副本,這樣才能在對應的delete運算符中引用該內存分配器,因為協程函數的參數並沒有被傳遞給delete運算符。這是由於參數存儲在協程幀中,而在調用delete運算符時它們就已經被析構。【譯註:對函數參數副本的析構發生在釋放(delete)協程幀之前。】

例如,你可以實現一個new運算符,以便它能夠在協程幀的後面分配一塊額外的空間,用於保存內存分配器的一個副本,這個副本接下來被用於釋放協程幀的內存。

舉例如下:


為了讓自定義的my_promise_type作用於第一個參數是std::allocator_arg的協程,你需要特化coroutine_traits類(詳情請參閱下文關於coroutine_traits的章節)。

就像下面這樣:


需要注意的是,即便你為協程自定義了內存分配策略,編譯器仍然被允許忽略使用你的內存分配器

複製參數到協程幀

協程需要將原始調用方傳遞給協程函數的每一個參數複製到協程幀,以便在協程被掛起後它們仍然保持有效。

如果參數是按值傳遞給協程的,那麼這些參數會通過調用其類型的移動構造函數複製到協程幀。

如果參數是按引用(無論左值引用還是右值引用)傳遞給協程的,那麼只有它們的引用被複制到協程幀,而不複製它們引用的值。

需要注意,對於具有平凡析構函數【譯註:trivial destructor,是C++中的一個術語,表示析構函數不執行任何操作。為了方便,我借用了數學用語,譯為平凡析構函數】的類型,如果該類型的參數在協程中的<return-to-caller-or-resumer>點之後再也不會被引用,那麼編譯器就可以自由刪除這類參數的副本。

當按引用向協程傳遞參數時會有許多坑,因為你未必能指望在協程生命周期內,這些引用會一直保持有效。在普通函數中常用的技術,比如完美轉發(perfect-forwarding)和通用引用(universal-references),如果被用在協程中,則可能導致代碼具有未定義的行為。如果你想了解更多信息,請參閱Toby Allsopp對此寫的一篇非常棒的文章【譯註:https://toby-allsopp.github.io/2017/04/22/coroutines-reference-params.html】。

如果任何一個參數的拷貝或移動構造函數拋出了異常,那麼所有已構造的參數都會被析構,協程幀會被釋放,而異常則會傳播給調用方。

構造promise對象

一旦所有參數都被複制到協程幀中,協程接著就會構造promise對象。

之所以要在promise對象構造之前複製參數,是為了讓promise對象在其構造函數中可以訪問這些複製後的參數。

首先,編譯器會檢查promise是否存在可以接受已複製參數左值引用的構造函數重載。如果編譯器找到了這樣的構造函數重載,那麼就生成調用該重載的代碼。如果找不到這樣的重載,則退而求其次,生成調用promise默認構造函數的代碼。

請注意,promise的構造函數可以「窺視」(peek)參數的能力是最近對協程技術規範的一個調整,這個調整於傑克遜維爾(Jacksonville)2018年會議上被接受進N4723工作草案。有關提案請參見P0914R1。所以,較舊版本的Clang或MSVC可能不支持這種能力。

如果promise的構造函數拋出了一個異常,那麼在異常傳播給調用方之前,堆棧展開(stack unwinding)期間,參數副本會被析構,協程幀則會被釋放。

獲取返回對象

協程對promise對象做得第一件事情就是通過調用promise.get_return_object()獲取返回對象(return-object)。【譯註:比如一個task<T >對象。】

當協程首次掛起或運行完成,且執行回到調用方時,這個返回對象(return-object)會被返回給協程函數的調用方。

你可以認為控制流大致如下所示:


請注意,我們需要在協程體啟動之前就要獲取這個返回對象,因為對resume()的調用可能在當前線程上,也可能在另外一個線程上,協程幀(包括promise對象)可能在coroutine_handle::resume()返回之前就被銷毀。所以,在協程體啟動之後才調用promise.get_return_object()是不安全的。

初始掛起點(initial-suspend point)

一旦協程幀初始化完畢,且已經獲得返回對象,協程接下來要做的就是執行語句co_await promise.initial_suspend();。

這個語句能夠讓promise類型的開發者控制協程應該在執行函數體之前暫停,還是立即執行函數體。

如果協程在初始掛起點這個當口暫停了,那麼稍後可以在你選擇的某個時機通過調用協程句柄上的resume()或destroy()恢復或銷毀該協程。

co_await promise.initial_suspend()表達式的結果會被丟棄,所以實現時一般是從其awaiter對象的await_resume()方法返回void。【譯註:因為promise.initial_suspend()的結果可以被co_await,那麼這個返回結果是一個awaiter對象,按照Awaiter介面的定義,這個對象上會有一個await_resume()方法。】

需要特別注意的是,這個語句位於保護著協程剩餘部分的try/catch塊之外(如果你忘記了它長什麼樣子,請向上滾動至定義協程體的地方)。這意味著,在到達其<return-to-caller-or-resumer>之前,任何從co_await promise.initial_suspend()產生的異常,都會在銷毀協程幀和銷毀返回對象之後拋給協程的調用方。

如果你的返回對象具有RAII語義,並且在其析構期間銷毀了協程幀,那麼就需要你格外小心。你必須確保co_await promise.initial_suspend()是noexcept的,以避免協程幀被重複釋放。

請注意,有提案建議調整該語義,以便能夠將co_await promise.initial_suspend()表達式的全部或部分包含到協程體的try/catch塊之內。因此確切的語義可能在協程正式完成之前發生改變。

對許多類型的協程,initial_suspend()方法要麼返回std::experimental::suspend_always(非同步操作延遲啟動時),要麼返回std::experimental::suspend_never(非同步操作立即啟動時)。這兩個對象都是noexcept的awaitable對象【譯註:這兩個類型的await_ready、await_suspend和await_resume三個方法都被聲明為noexcept】,所以一般不會有什麼問題。

返回調用方

當協程函數執行到其第一個<return-to-caller-or-resumer>點時(或沒有執行到任何此類點協程就運行完成時),從promise.get_return_object()方法獲取到的返回對象(return-object)就會被返回給協程的調用方。

需要注意的是,返回對象(return-object)的類型不需要與協程函數的返回類型相同。必要時,返回對象會被隱式轉換為協程函數的返回類型。

請注意,Clang的協程實現(從5.0開始)直到返回對象從協程調用中返回時才執行這一轉換。而MSVC從2017 Update 3開始的實現則是在調用get_return_object()後立即執行轉換。儘管技術規範對此沒有明確說明預期的行為,我相信MSVC計劃將他們的實現改成更像Clang的方式,因為這有助於實現一些有趣的應用場景【譯註:請參見https://github.com/toby-allsopp/coroutine_monad】。

使用co_return從協程返回

當協程執行到一個co_return語句時,該語句被轉換為promise.return_void()或promise.return_value(<expr>),之後跟著一個goto FinalSuspend;。

co_return的翻譯規則如下:

  • co_return; 轉換為 promise.return_void();
  • co_return <expr>; 如果 <expr> 類型是 void,轉換為 <expr>; promise.return_void();。如果 <expr> 類型不是 void,轉換為promise.return_value(<expr>);。

接下來,goto FinalSuspend;會導致所有具有自動存儲生命期的局部變數被析構,析構順序與它們之前被構造的順序相反。在這之後執行co_await promise.final_suspend();。

請注意,在協程沒有包含任何co_return語句的情況下,如果執行離開了協程末尾,那麼等效於在函數體最後包含一個co_return;。在這種情況下,如果promise類型沒有提供return_void()方法,那麼行為是未定義的。

如果對<expr>求值時,或調用promise.return_void()或promise.return_value()時發生了異常,那麼該異常仍然會被傳播給promise.unhandled_exception()(參見下文)。

處理從協程體拋出的異常

當異常從函數體拋出後,該異常會被捕捉,catch塊內的promise.unhandled_exception()方法會被調用。

這個方法的實現一般會調用std::current_exception()方法獲取異常的一個副本,然後保存這個副本以便稍後在一個不同的上下文中重新拋出。

也可以在這個方法中通過執行throw;語句立即重新拋出異常。示例請參見folly::Optional【譯註: 請參見https://github.com/facebook/folly/blob/4af3040b4c2192818a413bad35f7a6cc5846ed0b/folly/Optional.h#L587】。然而,這麼做將會(或可能,請參見下文)導致協程幀被立即銷毀,而異常則會被傳播給調用方或恢復程序。如果對協程的上層封裝假設或要求對coroutine_handle::resume()的調用是noexcept的,那麼就會帶來問題,因此通常只有在你可以完全掌控是誰或是什麼能夠調用resume()時才去使用這種方式。

請注意,對於在調用unhandled_exception()時重新拋出異常的情況(或者,try塊之外的任何邏輯拋出異常的情況),應該具有何種預期行為,當前技術規範的措辭(wording)還不是太明確【譯註:請參見https://github.com/GorNishanov/coroutines-ts/issues/17】。

目前我對技術規範措辭的解釋是,如果控制流退出了協程體,通過在co_await promise.initial_suspend()、promise.unhandled_exception()或co_await promise.final_suspend()中拋出異常,或者以co_await promise.final_suspend()同步完成的方式結束協程的運行,那麼在返回到調用方或恢復程序之前,協程幀會被自動銷毀。然而,這種解釋有其自身的問題。

協程規範的未來版本有望對這一問題進行澄清。不過,在那之前,我會避免從initial_suspend()、final_suspend()和unhandled_exception()中拋出異常。請持續關注規範的變化!(Stay tuned!)

最後掛起點(final-suspend point)

一旦執行退出了協程體用戶定義的那一部分,且結果已經通過return_void()、return_value()或unhandled_exception()捕獲,同時所有的局部變數也都已被銷毀,那麼在返回到調用方或恢復程序之前,協程有機會執行一些附加邏輯。

這時協程會執行co_await promise.final_suspend();語句。

這能夠讓協程可以做這樣的一些事,比如發布結果,發出完成信號,或恢復一個接續協程(continuation)【譯註:請參見下文中對接續協程的解釋】。還可以讓協程在執行完成且銷毀協程幀之前選擇立即掛起。

請注意,如果在final_suspend()點處掛起協程,那麼對其調用resume()會導致未定義的行為。對此處掛起的協程唯一能夠做的就是destroy()它。

按照Gor Nishanov的說法,進行此限制的基本理由(rationale)是,這可以減少需要協程表達的掛起狀態的個數,而且也可能減少所需的分支數,從而讓編譯器有機會做一些優化工作。

需要注意的是,雖然允許協程不必在final_suspend()處掛起,但是仍然建議你精心構建你的協程,以便讓它們在可能的情況下能夠在final_suspend()處掛起。這是因為,這能夠促使你在協程外部調用協程的.destroy()方法(通常是在某個RAII對象的析構函數中),同時也能夠讓編譯器更容易決定在什麼情況下協程幀的生命周期是嵌套在調用方生命周期裡面的。這反過來更有可能讓編譯器不必為協程幀分配內存。

編譯器如何選擇promise類型

現在,讓我們考察一下,給定一個協程,編譯器如何決定使用哪種類型的promise對象。

編譯器使用std::experimental::coroutine_traits類,根據協程簽名確定協程的promise對象類型。

如果你有如下簽名的一個協程函數:


那麼,編譯器會將返回類型和參數類型列表 作為模板參數 傳遞給 coroutine_traits 來推斷協程的 promise 類型。


如果協程函數是一個類的非靜態成員函數,那麼該類類型會被作為第二個模板參數傳遞給coroutine_traits。要注意,如果你的方法是右值引用的重載,那麼第二個模板參數也將是一個右值引用。

例如,如果你有如下兩個方法:


那麼編譯器將 分別 使用如下的 promise 類型:


coroutine_traits 模板的默認定義是通過查找定義在返回值類型上的 promise_type 類型別名 ( typedef ) 來確定協程 的 promise 類型 ,即 類似 如下的定義( 不過會使用某種額外的 SFINAE 魔法,以便在找不到 RET::promise_type 時,讓 coroutine_traits ::promise_type 是未定義的 )。 【譯註: SFINAE : Substitution Failure Is Not An Error ,匹配失敗不是錯誤。 】


於是,對於你可以控制的協程返回類型,你只需要為它們嵌套定義一個promise_type,編譯器就會將這個promise_type作為協程promise對象的類型,這個promise對象會返回你的類的對象實例【譯註:promise.get_return_object()返回一個task<>的對象實例】。

例如:


而對於你無法控制的協程返回類型,你可以通過特化coroutine_traits的方式指定協程要使用的promise類型,而無需修改這個返回類型。

比如,為一個返回std::optional<T>的協程指定promise類型,可以這樣定義:


識別特定的協程活動幀

當你調用一個協程函數,協程幀就會被創建。為了能夠恢復其關聯的協程,或銷毀協程幀,你需要某種標識或引用該特定協程幀的方式。

技術規範為此提供的機制就是協程句柄(coroutine_handle)類型。

此類型的簡化介面定義如下:


有兩種可以獲得協程句柄的方式:

  1. 在執行co_await 表達式期間,此句柄會被傳遞給await_suspend()方法。
  2. 如果你有一個協程promise對象的引用,那麼就可以使用coroutine_handle<Promise>::from_promise()重構目標協程的句柄。

當協程在co_await表達式的<suspend-point>處被掛起後,等待協程的句柄會被傳遞給awaiter對象的await_suspend()方法。你可以將這個coroutine_handle看作是接續傳遞風格(continuation-passing style)調用中對接續協程(the continuation of the coroutine)的表示。【譯註: Continuation-Passing Style(CPS),我們沒有找到一個約定俗成的詞來翻譯continuation,而這個continuation在許多位置都有使用。為了方便,我們將其翻譯為「接續」。如果原文中continuation單獨出現,根據上下文,我們會翻譯為「接續協程」,意思是,在當前協程完成之後,會在發布的結果的基礎上,接著繼續執行的另一個協程。我們非常希望能夠看到更合適的翻譯。】

請注意,coroutine_handle不是對象,當然也就不是RAII對象。你必須手動調用.destroy()方法銷毀協程幀,以及釋放相關的資源。請將其看作是管理內存用的void*等價物。這是出於性能方面的考慮:使其成為一個RAII對象會為協程增加額外的開銷,比如需要引用計數。

通常,你應該使用為協程提供RAII語義的更高級別的類型,比如cppcoro庫(無恥的自我推銷一下)【譯註:原文為shameless plug,這篇文章的作者也是cppcoro庫的作者,所以對藉機推銷進行一番調侃】提供的類型,或者為你的協程編寫自己的高級類型來封裝協程幀的生命周期。

自定義co_await的行為

promise類型還可以自定義出現在協程體內每一個co_await表達式的行為。

通過為promise類型定義一個名稱為await_transform()的方法,編譯器就會將協程方法體中每一個co_await <expr>轉換為co_await promise.await_transform(<expr>)。

這種方式有許多重要且強大的用處:

能夠讓你等待正常情況下不可等待的類型。

例如,返回類型是std::optional<T>的協程,其promise類型可能提供一個await_transform()重載,這個重載具有一個std::optional<U>參數,並且返回一個awaitable類型,這個awaitable要麼返回一個類型為U的值,要麼在值為nullopt時掛起協程。【譯註:或者我們在上一篇文章中舉的std::chrono::duration的例子。】


能夠讓你通過聲明一個已刪除的await_transform()重載,來禁用對某些類型的等待。

比如,返回類型是std::generator<T>的協程的promise類型可能會聲明一個已刪除的await_transform()模板成員函數,這個函數可以接受任何類型。這基本上禁用了在協程中對std::generator<T>使用co_await。【譯註:generator服務於co_yield,而不是co_await。std::generator<T>請參見P2168R0(2020-05-16)。】


能夠讓你為滿足新的情況而更改正常可等待值(normally awaitable)的行為。

例如,你可以定義這樣的一類協程,通過在一個resume_on()運算符(請參見cppcoro::resume_on())中封裝awaitable對象,確保協程總是會從一個關聯執行程序上的每一個co_await表達式中恢復。


關於await_transform(),最後需要重點強調的是,如果promise類型上定義了任何await_transform()成員,那麼就會觸發編譯器將所有的co_await表達式轉換為對promise.await_transform()的調用。這意味著,如果你只想針對某些類型自定義co_await的行為,那麼你還需要提供一個僅轉發參數的await_transform()的後備重載。

自定義co_yield的行為

您能夠通過promise類型自定義的最後一件事是co_yield關鍵字的行為。

如果co_yield關鍵字出現在了協程中,那麼編譯器會將co_yield <expr>表達式轉換為co_await promise.yield_value(<expr>)。於是可以通過在promise對象上定義一個或多個yield_value()方法來自定義co_yield關鍵字的行為。

請注意,與await_transform()不同,如果promise類型沒有定義yield_value()方法,那麼co_yield沒有默認的行為。因此,promise類型需要通過聲明已刪除的await_tranform()來顯式地選擇不支持co_await,也需要通過定義yield_value()來顯式地選擇支持co_yield。

具有yield_value()方法的promise類型的典型示例是generator<T>類型:


總結

在本文中,我介紹了將一個函數編譯為協程時,編譯器所做的各種轉換。

希望這篇文章能夠幫助你理解如何通過定義不同的你自己的promise類型來自定義不同類別協程的行為。協程機制提供了許多靈活的組件,通過它們,你可以使用許多不同的方式自定義協程的行為。

儘管如此,還有一個編譯器會執行的更重要的轉換我還沒有介紹——那就是將協程體轉換為狀態機。不過,鑒於這篇文章已經太長了,我將推遲到下一篇文章中對此進行解釋。敬請關注!(Stay tuned!)