Kotlin Coroutines 深入淺出 (一)

Sunny Yuan
Gogolook Tech
Published in
12 min readJul 21, 2020

Kotlin Coroutines 用來處理非同步任務,而在 Android 使用 Coroutines 需要注意生命週期,所以如何管理 Coroutines 生命週期以及處理 Cancellation 和 Exception 是很重要的,當 Coroutines 作用域為 View 相關或已經不需要使用時,應該要取消,避免資源的浪費甚至影響效能。

因此,Android KTX libraries 提供了搭配 Architecture components 使用的 CoroutineScope,例如 ViewModelScopeLifecycleScope 。它們會根據生命週期,在結束時自動取消 coroutines,我們不用自行處理。

然而實務上,除了使用這些 Component 外,我們也常會需要自行定義 CoroutineScope 來處理非同步任務的 Cancellation 和 Exception。這兩件事看起來很基本,就像 Coroutines 用起來簡單,但是深入鑽進去才發現,怎麼跟你想的不一樣?裡頭有個重要的概念就是 Coroutine cancellation 的傳播 (propagation) 機制。在介紹它們之前,本篇文章會重在探討 CoroutineContext 的組成以及 Coroutine parent-child 的關係。希望能為後續 Coroutines 的探討,打下一個良好的基礎。

CoroutineContext

CoroutineContext 用來定義 Coroutine 的行為,建構 CoroutineScope 時所傳入的參數,主要由四個元件組成:

  • CoroutineDispatcher
    coroutine 所執行的 thread 或 thread pool
  • Job
    掌管 coroutine 生命週期,有 6 種狀態:New, Active, Completing, Completed, Cancelling, Cancelled
  • CoroutineName
    coroutine 的名稱,用來 debugging,default 為 “coroutine”
  • CoroutineExceptionHandler
    處理 uncaught exceptions

CoroutineScope 可以新增 Coroutine,Coroutine 內又可以再新增多個 Coroutine。當新增 Coroutine 時會回傳一個 Job,這些 Job 們就會默默生成 task hierarchy

例如,先建一個 CoroutineScope:

// 新增 CoroutineScope
val scope = CoroutineScope(
Job() + Dispatchers.Main + coroutineExceptionHandler
)

CoroutineName 初始值為 “coroutine”,scope 的 CoroutineContext:

圖示:CoroutineContext

再由 scope 新增 Coroutine,傳入 context 參數 Dispatchers.IO

// launch Coroutine
val job = scope.launch(Dispatchers.IO) {
// new coroutine
}

在 CoroutineScope 內新增 Coroutine 時需先產出 parent context,再來建構新的 CoroutineContext,根據 Parent context 公式:

coroutine builder arguments 擁有較高優先權會覆寫 inherited context,也就是公式 “+” 右邊優先權大於左邊

Dispatchers.IO 會覆寫 Dispatchers.Main,這時新增的 Coroutine context 根據公式就會是:

New Coroutine context

Job 一定會是一個新的實例,原因是新增 coroutine 需要回傳一個 Job來控制生命週期。

那麼,如果 Parent context 會被覆寫的話,其他 child coroutine 在繼承 context 時也會一起改變嗎?

以下是實際執行的結果,拿 Dispatcher 當變數,看各個 coroutine 的 CoroutineContext。開啟 debug mode 來觀察:

// System.setProperty("kotlinx.coroutines.debug", "on")val scope: CoroutineScope = CoroutineScope(Job() + Dispatchers.Main)

val job1: Job = scope.launch(Dispatchers.IO) {
println("job1: $coroutineContext")
}

val job2: Job = scope.launch {
println("job2: $coroutineContext")

val job3: Job = launch(Dispatchers.Default) {
println("job3: $coroutineContext")
}

val job4: Job = launch {
println("job4: $coroutineContext")
}

}

Output:

job1: [CoroutineId(2), "coroutine#2":StandaloneCoroutine{Active}@928279e, LimitingDispatcher@60c6540[dispatcher = DefaultDispatcher]]
job2: [CoroutineId(3), "coroutine#3":StandaloneCoroutine{Active}@b6fb96f, Main]
job3: [CoroutineId(4), "coroutine#4":StandaloneCoroutine{Active}@3ebf67c, DefaultDispatcher]
job4: [CoroutineId(5), "coroutine#5":StandaloneCoroutine{Active}@3438971, Main]

job1 的 Dispatcher.IO 原始碼裡定義為 DefaultScheduler.IO,所以名稱是 DefaultDispatcher。job2 繼承 scope 的 context,所以是 Dispatcher.Main。
job4 繼承 job2 的 CoroutineContext,代表 job3 的 parent context 不會去覆寫 job2 的CoroutineContext。

// Dispatchers.kt
@JvmStatic
public val IO: CoroutineDispatcher = DefaultScheduler.IO

CoroutineScope

驗證完 parent-child CoroutineContext 的關係,再來從原始碼看看 CoroutineScope.launch 實際運作方式:

// CoroutineScope.kt
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}

CoroutineScope 是一個 interface,裡面只有一個 read-only val coroutineContext,大部分透過 extension functions 來實作

// Builders.common.kt
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}

CoroutineScope.newCoroutineContext 結合 scope 的 CoroutineContext 和參數 context 產生 newContext

// CoroutineContext.kt
@ExperimentalCoroutinesApi
public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
val combined = coroutineContext + context
val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined
return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)
debug + Dispatchers.Default else debug
}

接著看 CoroutineStart.DEFAULT 的狀況,coroutine 的實例是 StandaloneCoroutine,而傳入的 newContext 就是 Parent context

// Builders.common.kt
private open class StandaloneCoroutine(
parentContext: CoroutineContext,
active: Boolean
) : AbstractCoroutine<Unit>(parentContext, active) {
/* ... */
}

// AbstractCoroutine.kt
@InternalCoroutinesApi
public abstract class AbstractCoroutine<in T>(

@JvmField
protected val parentContext: CoroutineContext,
active: Boolean = true
) : JobSupport(active), Job, Continuation<T>, CoroutineScope {
/* ... */
}

StandaloneCoroutine 繼承 abstract class AbstractCoroutine,而 AbstractCoroutine 實作 Job 和 CoroutineScope,也就是 Coroutine 其實就是 Job,所以 CoroutineScope.launch 會回傳一個 Job。

此外,Coroutine 實作了 CoroutineScope,所以我們在 Coroutine 內可以透過 CoroutineScope.launchCoroutineScope.async 來新增 Coroutine。

結論

CoroutineScope 為 Coroutine 作用域,Coroutines 的使用一定需要在 CoroutineScope 內。
CoroutineContext 定義了 coroutine 的行為,比較微妙的是包含了 Job 也就是 coroutine 自己,在內部透過 parentContext[Job] 可以取得 parent coroutine。Parent context 命名個人認為容易誤以為指的是 parent Job 的 CoroutineContext,但可能是不一樣的,根據 Coroutine context 公式,其實 Parent context 跟 Coroutine context,只差在 Job 實例不一樣而已。
Coroutines 結構中,每個 coroutine 都有 parent,可能是 CoroutineScope 或其他 coroutines。
Coroutines 各元件之間的關係比較抽象,希望透過本篇文章定義清楚元件的作用,了解基本概念和 Coroutine parent-child 關係後,下一篇再介紹 Cancellation 和 Exception 。

--

--