要讓 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