理解 Kotlin 協程:自底向上的視角

Kotlin 的協程應該是 Java 生態中最好的協程實現,在生產環境( Android / 後端場景)也有比較多實際應用。在移動 Android 開發中, Google 宣導 Kotlin-First 。而在後端開發中, Spring 5 / Spring Boot 一等公民支持 Kotlin 語言,也

添加了對 Kotlin 協程的支持 ;相信有後端開發框架王者 Spring 的加持, Kotlin 語言與 Kotlin 協程的發展普及有著樂觀的前景。

無論是 Kotlin 語言還是 Kotlin 協程,都非常注重務實與開發者友好:

  • Kotlin 語言:相對 Java ,方便簡潔(如字符串插值、 smart cast 、 data class 、統一原生類型與包裝類型、擴展方法、命名參數、默認參數)與安全(如非空類型)。
  • Kotlin 協程:以大家習慣的命令式/過程式的編程方式寫出非阻塞的高效併發程序。

但併發編程是計算機最複雜的主題之一,即使是用協程的編寫方式;再者 Kotlin 協程的友好使用方式,對於使用者理解協程背後的運行機制其實反而是個障礙。而真正的理解協程才能讓使用協程做到心中有數避免踩坑。這篇文章自底向上視角的講解方式,正是有意於正面解決這個問題:如何有效理解 Kotlin 協程運行機制。

考慮到原文中只給了代碼示例但沒有給出代碼工程,不方便讀者直接運行起來以跟著文章自己探索,譯者提供了示例代碼的可運行代碼工程,參見 GitHub 倉庫:

oldratlee/kotlin-coroutines-bottom-up

自己理解有限,翻譯中不足和不對之處,歡迎建議( 提交Issue )和指正( Fork後提Pull Request )。

理解 Kotlin 協程:自底向上的視角

理解 Kotlin 協程:自底向上的視角

  • 2.1 示例應用(服務端)2.2 示例應用(客戶端)2.3 訪問 REST endpoint 的服務
  • 3.1 延續傳遞風格( Continuation Passing Style / CPS )3.2 掛起還是不掛起 —— 這是一個問題3.3 大 switch 語句( The Big Switch Statement )和標籤
  • 4.1 第三次調用 fetchNewName 的請求 —— 不掛起4.2 第三次調用 fetchNewName —— 掛起

0. 關鍵要點

  • JVM 沒有原生支持協程( coroutines )
  • Kotlin 實現協程方式是,在編譯器中將代碼轉換為狀態機
  • 實現協程, Kotlin 語言只用了一個關鍵字,剩下的通過庫來實現
  • Kotlin 使用延續傳遞風格( Continuation Passing Style / CPS )來實現協程
  • 協程使用了調度器( Dispatchers ),因此在 JavaFX 、 Android 、 Swing 等中使用方式略有不同

協程是一個令人著迷的主題,儘管並不是一個新話題。正如 其他地方提到的 ,協程這些年來已經被多次重新發現挖掘出來,通常是作為輕量級線程( lightweight threading )或『回調地獄』( callback hell )的解決方案。

最近在 JVM 上,協程已成為反應式編程( Reactive Programming )的一種替代方法。諸如 RxJava Project Reactor 之類的框架為客戶端提供了一種增量處理傳入信息的方式,並且對節流( throttling )和並行( parallelism )提供了廣泛的支持。但是,您必須圍繞反應流( reactive streams )上的函數式操作( functional operations )來重新組織代碼, 在很多情況下這樣做成本是大於收益的

這就是為什麼像 Android 社區會對更簡單的替代方案有需求的原因。 Kotlin 語言引入協程作為一個實驗功能來滿足這個需求,並且經過改進後已成為 Kotlin 1.3 的正式功能。 Kotlin 協程的採用範圍已從 UI 開發拓展到服務器端框架(比如 Spring 5 添加了支持 ),甚至是像 Arrow 之類的函數式框架(通過 Arrow Fx )。

1. 協程的理解挑戰

不幸的是理解協程並非易事。儘管有非常多 Kotlin 專家的協程分享,但主要是關於協程是什麼(或是協程的用法)這方面的見解和介紹。你可能會說協程是並行編程的單子 。

而要理解協程有挑戰的其實是底層實現。在 Kotlin 協程,編譯器僅實現 suspend 關鍵字,其他所有內容都由協程庫處理。結果是, Kotlin 協程非常強大和靈活,但同時也顯得用無定形。對於新手來說,這是學習障礙,而在初學一個東西時就給到固定一致的準則和原則是最好的。本文有意於提供這個基礎,自底向上地介紹協程。

2. 示例應用

2.1 示例應用(服務端)

示例應用是一個典型問題:安全有效地對 RESTful 服務進行多次調用。播放 《威利在哪裡?》 的文字版 —— 用戶要追蹤一個連著一個的人名鏈,直到出現 Waldo 。

【譯註】《威利在哪裡?》是一套由英國插畫家 Martin Handford 創作的兒童書籍。這個書的目標就是在一張人山人海的圖片中找出一個特定的人物 —— 威利。他總是會弄丟東西,如書本、野營設備甚至是他的鞋子,而讀者也要幫他在圖中找出這些東西來。更多參見 威利在哪裡 - 百度百科威利在哪裡 - 維基百科

下面是用 Http4k 編寫的完整 RESTful 服務實現。 Http4k 是 Marius Eriksen 的 著名論文 中所寫的函數式服務端架構的 Kotlin 版本實現。實現有許多其他語言,包括 Scala ( Http4s )和 Java 8 或更高版本( Http4j )。

實現有唯一一個 endpoint ,通過 Map 實現人名鏈。給定一個人名,返回匹配值和200狀態代碼,或是返回404和錯誤消息。

<code>fun main() {   val names = mapOf(       "Jane" to "Dave",       "Dave" to "Mary",       "Mary" to "Pete",       "Pete" to "Lucy",       "Lucy" to "Waldo"   )   val lookupName = { request: Request ->       val name = request.path("name")       val headers = listOf("Content-Type" to "text/plain")       val result = names[name]       if (result != null) {           Response(OK)               .headers(headers)               .body(result)       } else {           Response(NOT_FOUND)               .headers(headers)               .body("No match for $name")       }   }   routes(       "/wheresWaldo" bind routes(           "/{name:.*}" bind Method.GET to lookupName       )   ).asServer(Netty(8080))       .start()}/<code>

【譯註】上面示例代碼 完整實現的工程文件: ServerMain.kt

也就是說,用戶要完成的操作是執行下面的請求鏈:

<code>$ curl http://localhost:8080/wheresWaldo/MaryPete$ curl http://localhost:8080/wheresWaldo/PeteLucy$ curl http://localhost:8080/wheresWaldo/LucyWaldo/<code>

2.2 示例應用(客戶端)

客戶端應用基於 JavaFX 庫來創建桌面用戶界面,為了簡化任務並避免不必要的細節,使用 TornadoFX ,它為 JavaFX 提供了 Kotlin 的 DSL 實現。

下面是客戶端視圖的完整定義:

<code>class HelloWorldView: View("Coroutines Client UI") {   private val finder: HttpWaldoFinder by inject()   private val inputText = SimpleStringProperty("Jane")   private val resultText = SimpleStringProperty("")   override val root = form {       fieldset("Lets Find Waldo") {           field("First Name:") {               textfield().bind(inputText)               button("Search") {                   action {                       println("Running event handler".addThreadId())                       searchForWaldo()                   }               }           }           field("Result:") {               label(resultText)           }       }   }   private fun searchForWaldo() {       GlobalScope.launch(Dispatchers.Main) {           println("Doing Coroutines".addThreadId())           val input = inputText.value           val output = finder.wheresWaldo(input)           resultText.value = output       }   }}/<code>

【譯註】上面示例代碼 完整實現的工程文件: HelloWorldView.kt

另外還使用了下面的輔助函數作為 String 類型的擴展方法:

<code>fun String.addThreadId() = "$this on thread ${Thread.currentThread().id}"/<code>

【譯註】上面示例代碼 完整實現的工程文件: Utils.kt

下面是用戶界面運行起來的樣子:

理解 Kotlin 協程:自底向上的視角

【譯註】在可運行代碼工程中,通過文件 MyApp.kt 的 main 啟動客戶端。

當用戶單擊按鈕時,會啟動一個新的協程,並通過類型為 HttpWaldoFinder 的服務對象訪問 RESTful endpoint 。

Kotlin 協程存在於 CoroutineScope 之中, CoroutineScope 關聯了表示底層併發模型的 Dispatcher 。併發模型通常是線程池,但可以是其它的。

有哪些 Dispatcher 可用取決於 Kotlin 代碼的所運行環境。 Main Dispatcher 對應的是 UI 庫的事件處理線程,因此(在 JVM 上)僅在 Android 、 JavaFX 和 Swing 中可用。 Kotlin Native 的協程在開始時完全不支持多線程, 但是這種情況正在改變 。在服務端,可以自己引入協程,但是缺省就可用情況會越來越多, 比如在 Spring 5 中

在開始調用掛起方法( suspending methods )之前,必須要有一個協程、一個 CoroutineScope 和一個 Dispatcher 。如果是最開始的調用(如上面的代碼所示),可以通過『協程構建器』( coroutine builder )函數(如 launch 和 async )來啟動這個過程。

調用協程構建器函數或諸如 withContext 之類的作用域函數( scoping function )總會創建一個新的 CoroutineScope 。在這個上下文中,執行的任務( task )對應由 Job 實例組成的層次結構。

協程的任務具有一些有趣的特性,即:

  • Job 在自己完成之前,會等待自己區域中的所有協程完成。
  • 取消 Job 導致其所有子 Job 被取消。
  • Job 的失敗或取消會傳播給他的父 Job 。

這樣的設計是為了避免併發編程中的常見問題,比如在沒有終止子任務的情況下終止了父任務。

2.3 訪問 REST endpoint 的服務

下面是 HttpWaldoFinder 服務的完整代碼:

<code>class HttpWaldoFinder : Controller(), WaldoFinder {   override suspend fun wheresWaldo(starterName: String): String {       val firstName = fetchNewName(starterName)       println("Found $firstName name".addThreadId())       val secondName = fetchNewName(firstName)       println("Found $secondName name".addThreadId())       val thirdName = fetchNewName(secondName)       println("Found $thirdName name".addThreadId())       val fourthName = fetchNewName(thirdName)       println("Found $fourthName name".addThreadId())       return fetchNewName(fourthName)   }   private suspend fun fetchNewName(inputName: String): String {       val url = URI("http://localhost:8080/wheresWaldo/$inputName")       val client = HttpClient.newBuilder().build()       val handler = HttpResponse.BodyHandlers.ofString()       val request = HttpRequest.newBuilder().uri(url).build()       return withContext<string>(Dispatchers.IO) {           println("Sending HTTP Request for $inputName".addThreadId())           client               .send(request, handler)               .body()       }   }}/<string>/<code>

【譯註】上面示例代碼 完整實現的工程文件: WaldoFinder.kt

fetchNewName 函數的參數是已知人名,在 endpoint 中查詢關聯的人名。通過 HttpClient 類完成,這個類在 Java 11 及以後版本的標準庫中。實際的 HTTP GET 操作在使用 IO Dispatcher 的新子協程中運行。 IO Dispatcher 代表了為長時間運行的活動(如網絡調用)優化的線程池。

wheresWaldo 函數追蹤人名稱鏈五次,以(期望能)找到 Waldo 。因為後面要反彙編生成的字節碼,所以邏輯實現得儘可能簡單。我們感興趣的是,每次調用 fetchNewName 都會導致當前協程被掛起,儘管它的子協程會在運行。在這種特殊情況下,父協程在 Main Dispatcher 上運行,而子協程在 IO Dispatcher 上運行。因此,儘管子協程在執行 HTTP 請求, UI 事件處理線程已經釋放了,以處理其他用戶與視圖的交互。如下圖所示:

理解 Kotlin 協程:自底向上的視角

當調用掛起函數時, IntelliJ 會在編輯器窗口左邊條用圖標提示在協程之間有控制權轉移。請注意,如果不切換 Dispatcher ,則調用不一定會導致新協程的創建。當一個掛起函數調用另一個掛起函數時,可以在同一協程中繼續執行,實際上,如果處在同一線程上,這就是我們想要的行為。

理解 Kotlin 協程:自底向上的視角

當執行客戶端時,控制檯的輸出如下:

<code>Sending HTTP Request for Lucy on thread 24Running event handler on thread 17Doing Coroutines on thread 17Sending HTTP Request for Jane on thread 24Found Dave name on thread 17Sending HTTP Request for Dave on thread 24Found Mary name on thread 17Sending HTTP Request for Mary on thread 24Found Pete name on thread 17Sending HTTP Request for Pete on thread 26Found Lucy name on thread 17Sending HTTP Request for Lucy on thread 26/<code>

可以看到,對於上面的這次運行, Main Dispatcher / UI 事件 Handler 在線程17上運行,而 IO Dispatcher 在包含線程24和26的線程池上運行。

3. 開始探索

使用 IntelliJ 自帶的字節碼反彙編工具,可以窺探底層的實際情況。當然你也可以使用 JDK 自帶的標準 javap 工具。

理解 Kotlin 協程:自底向上的視角

可以看到 HttpWaldoFinder 的方法簽名已經改變了,因此可以接受延續對象( continuation object )作為額外的參數,並返回一個通用的 Object 。

<code>public final class HttpWaldoFinder extends Controller implements WaldoFinder {  public Object wheresWaldo(String a, Continuation b)  final synthetic Object fetchNewName(String a, Continuation b)}/<code>

現在,讓我們深入研究添加到這些方法中的代碼,並解釋『延續( Continuation )』是什麼,以及返回的是什麼。

3.1 延續傳遞風格( Continuation Passing Style / CPS )

正如 Kotlin 標準化流程(也稱為 KEEP )中的 協程提案 所記錄的,協程的實現基於『延續傳遞風格』。延續對象用於存儲函數在掛起期間所需的狀態。

掛起函數的每個局部變量都成為延續的字段。另外還需要為各個函數的參數和當前對象(如果函數是方法)創建字段。因此,有四個參數和五個局部變量的掛起方法有至少十個字段的延續。

對於 HttpWaldoFinder 中的 wheresWaldo 方法,只有一個參數和四個局部變量,因此延續實現類型具有六個字段。如果將 Kotlin 編譯器生成的字節碼反彙編為 Java 源代碼,可以看到確實如此:

<code>$continuation = new ContinuationImpl($completion) {  Object result;  int label;  Object L$0;  Object L$1;  Object L$2;  Object L$3;  Object L$4;  Object L$5;  @Nullable  public final Object invokeSuspend(@NotNull Object $result) {     this.result = $result;     this.label |= Integer.MIN_VALUE;     return HttpWaldoFinder.this.wheresWaldo((String)null, this);  }};/<code>

由於所有字段都為 Object 類型,因此如何使用它們並不是很明顯。但是隨著我們進一步探索,可以看到:

  • L$0 持有對 HttpWaldoFinder 實例的引用。始終有值。
  • L$1 持有 starterName 參數的值。始終有值。
  • L$2 到 L$5 持有局部變量的值。在代碼執行時逐步填充。 L$2 將持有 firstName 的值,依此類推。

另外還有用於最終結果的字段,以及一個名為 label 引人注意的整型字段。

3.2 掛起還是不掛起 —— 這是一個問題

在檢查生成的代碼時,需要記住:整個過程必須處理兩個用例。每當一個掛起函數在調用另一個掛起函數時,要麼掛起當前協程(以便另一個可以在同一線程上運行),要麼繼續執行當前協程。

考慮一個從數據存儲中讀取的掛起函數,在發生 IO 操作時這個函數起很可能會掛起,但也可能讀取的是緩存的結果。後續調用可以同步返回緩存的值,而不會掛起。 Kotlin 編譯器生成的代碼對於這兩個路徑必須都是允許的。

Kotlin 編譯器調整了每個掛起函數的返回類型,以便可以返回實際結果或特殊值 COROUTINE_SUSPENDED 。對於後一種情況下當前的協程是被掛起的。這就是掛起函數的返回類型從結果類型更改為 Object 的原因。

我們的示例應用 wheresWaldo 會重複調用 fetchNewName 。從理論上講,這些調用中的都可以掛起或不掛起當前的協程。在寫 fetchNewName 的時候,我們知道的是掛起總是會發生。但是,如果要理解所生成的代碼,我們必須記住,實現需要能夠處理所有可能性。

3.3 大 switch 語句( The Big Switch Statement )和標籤

如果進一步查看反彙編的代碼,會發現埋在多個嵌套標籤( label )中的 switch 語句。這是狀態機( state machine )的實現,用於控制 wheresWaldo() 方法中的不同掛起點。下面的代碼用於說明 switch 語句的高層結構:

代碼段 1 :生成的 switch 語句與標籤

<code>String firstName;String secondName;String thirdName;String fourthName;Object var11;Object var10000;label48: {  label47: {    label46: {        Object $result = $continuation.result;        var11 = IntrinsicsKt.getCOROUTINE_SUSPENDED();        switch($continuation.label) {        case 0:            // code omitted        case 1:            // code omitted        case 2:            // code omitted        case 3:            // code omitted        case 4:            // code omitted        case 5:            // code omitted        default:           throw new IllegalStateException(                "call to 'resume' before 'invoke' with coroutine");        } // end of switch        // code omitted    } // end of label 46    // code omitted  } // end of label 47  // code omitted} // end of label 48// code omitted/<code>

現在可以看清楚延續的 label 字段的用途。當完成 wheresWaldo 的不同階段時,更改 label 的值。嵌套的標籤塊包含原來 Kotlin 代碼中掛起點之間的代碼塊。 label 的值允許重新進入該代碼:跳到上次掛起的位置(適當的 case 語句),從延續中提取一些數據,然後開始執行對應的標籤塊代碼 。

但是,如果所有的掛起點都沒有實際掛起,則整個代碼塊會同步執行掉。在生成的代碼中,下面的代碼片段反覆出現:

代碼段 2 :決定當前協程是否應該掛起

<code>if (var10000 == var11) {  return var11;}/<code>

從之前的代碼可以知道, var11 的值是 CONTINUATION_SUSPENDED ,而 var10000 保存的是調用另一個掛起函數的返回值。因此,當發生掛起時,代碼將返回(之後會重新進入);如果沒有發生掛起,代碼會穿過了一個標籤,直接繼續執行這個標籤之後的代碼。

再強調一次,請記住:生成的代碼不能假定所有調用都將掛起,或者所有調用都將繼續執行當前協程。必須能夠應對任何可能的組合。

4. 追蹤執行過程

當執行開始時,延續中 label 字段的值設置的是 0 。 switch 語句中相應分支代碼如下:

代碼段 3 : switch 語句的第一個分支

<code>case 0:  ResultKt.throwOnFailure($result);  $continuation.L$0 = this;  $continuation.L$1 = starterName;  $continuation.label = 1;  var10000 = this.fetchNewName(starterName, $continuation);  if (var10000 == var11) {    return var11;  }  break;/<code>

將實例和參數存儲到延續對象中,然後將延續對象傳遞給 fetchNewName 。如前文所述,編譯器生成的 fetchNewName 版本將返回實際結果或 COROUTINE_SUSPENDED 值。

如果協程被掛起,那麼從函數中返回,並且當協程繼續時跳轉到 case 1 分支。如果繼續執行當前的協程(即沒有被掛起),那麼將穿過 switch 語句的一個標籤,執行下面的代碼:

代碼段 4 :第二次調用 fetchNewName

<code>firstName = (String)var10000;secondName = UtilsKt.addThreadId("Found " + firstName + " name");boolean var13 = false;System.out.println(secondName);$continuation.L$0 = this;$continuation.L$1 = starterName;$continuation.L$2 = firstName;$continuation.label = 2;var10000 = this.fetchNewName(firstName, $continuation);if (var10000 == var11) {  return var11;}/<code>

因為知道 var10000 是函數返回值,所以可以將其轉型為正確的類型並存儲在本地變量 firstName 中。然後,生成的代碼使用變量 secondName 存儲連接線程 ID 的結果,然後將其打印出來。

更新延續中的字段,以添加從服務器檢索到的值。請注意, label 的值現在為2。然後,我們第三次調用 fetchNewName 。

4.1 第三次調用 fetchNewName 的請求 —— 不掛起

我們必須再次基於 fetchNewName 返回的值進行選擇,如果返回的值是 COROUTINE_SUSPENDED ,那麼我們將從當前函數返回。下次調用時,我們將進入 switch 語句的 case 2 分支。

如果我們繼續當前的協程,則執行下面的代碼塊。正如您所看到的,它與上面的相同,除了我們現在有更多數據要存儲在延續中。

代碼段 4 :第三次調用 fetchNewName

<code>secondName = (String)var10000;thirdName = UtilsKt.addThreadId("Found " + secondName + " name");boolean var14 = false;System.out.println(thirdName);$continuation.L$0 = this;$continuation.L$1 = starterName;$continuation.L$2 = firstName;$continuation.L$3 = secondName;$continuation.label = 3;var10000 = this.fetchNewName(secondName, (Continuation)$continuation);if (var10000 == var11) {  return var11;}/<code>

對於所有剩餘的調用(假設從未返回 COROUTINE_SUSPENDED ),此模式將重複進行,直到到達結尾為止。

4.2 第三次調用 fetchNewName —— 掛起

另一情況是協程被掛起,那麼要運行下面的 case 塊:

代碼段 5 : switch 語句的第三個分支

<code>case 2:  firstName = (String)$continuation.L$2;  starterName = (String)$continuation.L$1;  this = (HttpWaldoFinder)$continuation.L$0;  ResultKt.throwOnFailure($result);  var10000 = $result;  break label46;/<code>

從延續中提取值到函數的局部變量中。然後用帶標籤的 break 將執行跳轉到上面的代碼段4。因此,最終將在同一個地方結束整個協程的執行。

4.3 執行過程總結

現在我們可以重新梳理代碼結構,並對每個部分中所做的進行高層的描述:

代碼段 6 :生成的 switch 語句與標籤,帶展開深入的說明

<code>String firstName;String secondName;String thirdName;String fourthName;Object var11;Object var10000;label48: {  label47: {     label46: {        Object $result = $continuation.result;        var11 = IntrinsicsKt.getCOROUTINE_SUSPENDED();        switch($continuation.label) {        case 0:            // 設置延續的`label`字段值 成1,進行第一次調用`fetchNewName`            // 如果fetchNewName調用掛起,則返回;否則執行switch的break        case 1:            // 從延續中提提取 參數值            // 執行switch的break(即跳過`switch`代碼塊)        case 2:            // 從延續中提取 參數值 以及 第一個結果            // 執行外面`label46`標籤的break(即跳過`label46`的代碼塊)        case 3:            // 從延續中提取 參數值 以及 第一個和第二個結果            // 執行外面`label47`標籤的break(即跳過`label47`的代碼塊)        case 4:            // 從延續中提取 參數值 以及 第一個、第二個和第三個結果            // 執行外面`label48`標籤的break(即跳過`label48`的代碼塊)        case 5:            // 從延續中提取 參數值 以及 第一個、第二個、第三個和第四個結果            // 返回最終的結果        default:           throw new IllegalStateException(                "call to 'resume' before 'invoke' with coroutine");        } // end of switch        // 將參數 以及 第一個結果 存入 延續        // 設置 延續的`label`字段值 成2,進行第二次調用`fetchNewName`        // 如果fetchNewName調用掛起,則返回;否則繼續執行    } // end of label 46        // 將參數 以及 第一個和第二個結果 存入 延續        // 設置 延續的`label`字段值 成3,進行第三次調用`fetchNewName`        // 如果fetchNewName調用掛起,則返回;否則繼續執行  } // end of label 47        // 將參數 以及 第一個、第二個和第三個結果 存入 延續        // 設置 延續的`label`字段值 成4,進行第四次調用`fetchNewName`        // 如果fetchNewName調用掛起,則返回;否則繼續執行} // end of label 48// 將參數 以及 第一個、第二個、第三個和第四個結果 存入 延續// 設置 延續的`label`字段值 成5,進行第五次調用`fetchNewName`// 返回 最終結果 或是 COROUTINE_SUSPENDED/<code> 

5. 結論

這不是一份容易理解的代碼。我們研究了由 Kotlin 編譯器生成的字節碼所反彙編的 Java 代碼。 Kotlin 編譯器生成的字節碼目標是執行的效率和簡約,而不是清晰易讀。

我們可以得出一些有用的結論:

  1. 沒有魔法 。當初次瞭解協程時,很容易會以為有一些特殊的『魔法』來把所有東西連接起來。正如我們上面所看到的,生成的代碼只使用過程式編程( Procedural Programming )的基本構建塊,比如條件語句和帶有標籤的 break 。
  2. 實現是基於延續的 。如 KEEP 提案中所說明,實現函數的掛起和恢復的方式是將函數的狀態捕捉到一個對象中。因此,對於每一個掛起函數,編譯器會創建一個具有 N 個字段的延續類,其中 N 是函數參數個數加上函數變量個數加上3。後面3個字段分別持有的是當前對象、最終結果和索引。
  3. 執行始終遵循標準模式 。如果要從掛起中恢復,使用延續的 label 字段跳轉到 switch 語句裡的對應分支。在這個分支中,從延續對象中取出所找到的數據,然後用帶有標籤的 break 語句跳轉到如果沒有發生掛起所要執行的代碼。


分享到:


相關文章: