CoroutineScope 描述的是 Coroutine 的執行範圍,而 Coroutine 在這個範圍內運作時所攜帶的資訊,則放在 CoroutineContext 裡。每個 Coroutine 都有自己的 Context,內容會在 suspend / resume 時被妥善保存與還原。
Continuation 攜帶的資訊#
Kotlin 採用無堆疊協程(Stackless Coroutine)的設計:每個 suspend 函式內部都有一個 Continuation 實例。當 Coroutine 因為某個 suspend 函式被掛起時,當下的上下文會存進 Continuation;等任務完成,再透過 Continuation 把資訊帶回原本的 Coroutine 繼續執行。
這個「上下文」其實就是 CoroutineContext,它是一組 Element 的集合,常見的 Element 包含:
Job:代表這個 Coroutine 的生命週期,可以 cancel、join。CoroutineDispatcher(屬於ContinuationInterceptor):決定 Coroutine 在哪個執行緒或執行緒池上跑。CoroutineName:給 Coroutine 一個方便除錯的名字。CoroutineExceptionHandler:處理 Coroutine 中未被捕捉的例外。
Coroutine 之所以能比執行緒輕量,正是因為切換時只需要把這個 Context(透過 Continuation)保存下來,不必像作業系統切換執行緒那樣保存整個呼叫堆疊。
用 + 組合 Context#
CoroutineContext 之間可以用 + 運算子組合,後面的 Element 會覆蓋同型別的前者。例如:
val scope = CoroutineScope(Job() + Dispatchers.Default + CoroutineName("worker"))這個 Scope 的 Context 同時包含 Job、Dispatcher 與 CoroutineName。launch、async、withContext 等地方傳入的 Context 也是同樣的合成方式。
子 Coroutine 繼承父 Context#
在 Scope 內透過 launch / async 建立的 Coroutine,預設會繼承父 Coroutine 的 Context;如果 launch 額外帶入 Context(例如 launch(Dispatchers.IO)),就會把對應的 Element 替換掉。這是 Coroutine 結構化併發的基礎:
- 父 Coroutine 被取消,子 Coroutine 也會被取消。
- 子 Coroutine 沒處理的例外,可以由父 Coroutine 的
CoroutineExceptionHandler接住。
CoroutineScope 與 CoroutineContext 的關係#
可以這樣理解兩者的角色分工:
CoroutineScope:定義 Coroutine 的執行範圍;它的核心其實就是持有一個coroutineContext屬性。CoroutineContext:實際攜帶 Coroutine 運作所需的設定(Job、Dispatcher、Name、ExceptionHandler⋯⋯)。
換句話說,Scope 是「在哪裡」、Context 是「帶著什麼」。
小結#
- CoroutineContext 是一組 Element 的集合,描述 Coroutine 運作所需的所有資訊。
- 常見 Element:Job、CoroutineDispatcher(屬於 ContinuationInterceptor)、CoroutineName、CoroutineExceptionHandler。
- Element 之間可以用
+組合;子 Coroutine 預設繼承父 Coroutine 的 Context。 - Coroutine suspend 時,這個 Context 會透過 Continuation 保存;resume 時再還原回來。
原文出處#
- 原書/iThome:https://ithelp.ithome.com.tw/articles/10262296