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