要讓 suspend 函式跑起來,必須有一個 CoroutineScope;CoroutineScope 描述的是「Coroutine 在這個範圍內執行」這件事。透過 launch、async 這些 Coroutine builder,可以在 Scope 中建立新的 Coroutine。

本章從 Coroutine 的本質切入,說明為什麼需要 CoroutineScope,以及它與 suspend / resume 機制的關係。

Coroutine 是協同式多工#

Coroutine 字面上是 cooperation(協同)加 routine(程序)的合成詞,意思是協同處理多個程序。維基百科的定義是:

Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing execution to be suspended and resumed.

也就是說,Coroutine 是在語言層面實作的非搶佔式多工機制,允許執行被暫停、再恢復。

搶佔式多工 vs 協同式多工#

  • 搶佔式多工(Preemptive multitasking):由作業系統根據執行緒優先權,把 CPU 時間分配給不同任務;高優先權的任務可以中斷低優先權的任務。執行緒的調度由系統決定。
  • 協同式多工(Cooperative multitasking):每個程式必須定時主動放棄執行權,告知系統可以切換到下一個任務。Coroutine 的調度由 Coroutine 自己決定,因為作業系統根本不知道 Coroutine 的存在。

Kotlin 的 Coroutine 屬於協同式多工。

suspend / resume 與 Continuation#

執行緒之間切換時,作業系統需要把當下的呼叫堆疊(call stack)保存下來,之後切回來才能繼續執行;這個保存與恢復是有成本的。

Kotlin 的 Coroutine 採用無堆疊協程(Stackless Coroutine)的設計:每個 suspend 函式內部隱含一個 Continuation 實例。當 Coroutine 暫停時,會把當下的上下文資訊存入 Continuation;suspend 函式完成後,再透過 Continuation 把控制權交回原本的 Coroutine。

換句話說,Continuation 本質就是一個 callback。Coroutine 因為只需要保存一個 Continuation 物件,相對於執行緒切換更輕量。

為什麼 suspend 函式必須在 Scope 內#

正因為 Coroutine 仰賴 Continuation 來保存上下文,suspend 函式必須在 CoroutineScope 中才能呼叫——CoroutineScope 隱含了那個負責切換的 Continuation 機制。在沒有 Scope 的地方直接呼叫 suspend 函式,編譯就無法通過。

實務上有幾種建立 Scope 的方式:

  • 使用 runBlocking { ... } 啟動一個阻塞當前執行緒、直到 Coroutine 完成的 Scope。
  • 使用 coroutineScope { ... } 在 suspend 函式中建立子 Scope,繼承外層 Context。
  • 直接以 CoroutineScope(...) 建立可被持有的 Scope(例如綁在某個物件的生命週期上)。

而在 Scope 中,再用 launch、async 建立實際執行的 Coroutine。

小結#

  • Coroutine 是協同式多工:何時讓出執行權由 Coroutine 自行決定。
  • Kotlin 採用無堆疊協程設計,suspend 時用 Continuation 保存上下文,相較於執行緒更輕量。
  • CoroutineScope 提供 suspend 函式運作所需的 Continuation 機制;所有 suspend 函式都必須在 Scope 中執行。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10262296