走完前幾節之後,Coroutine(協程)的形狀已經算清晰:它解決非同步程式的可讀性問題,採協同式多工(Cooperative Multitasking),用無堆疊協程(Stackless Coroutine)的方式實作。本節把實作面落地,把 Kotlin Coroutine 的三個核心要素整理成一份地圖:CoroutineScope、suspend 函式(suspend function)、Dispatcher(調度器)。

三大要素總覽#

要素角色
CoroutineScope規範 Coroutine 的生存範圍,所有 Coroutine 都得在某個 scope 內
suspend function標記可暫停的函式,描述「會等待」的工作
Dispatcher決定 Coroutine 在哪條執行緒(Thread)上執行

這三個要素是任何 Coroutine 程式都會用到的基本骨架。後面討論 builders、結構化併發、Channel、Flow 時,都建立在它們之上。

CoroutineScope#

CoroutineScope 描述「這群 Coroutine 該活多久」。常見的建立方式有三種:

  • coroutineScope { ... }:建立一個新的 scope,但不啟動新的 Coroutine。內部的 CoroutineContext 是從外部繼承來的,常用來把多個並行任務組合成一個。
  • launch { ... }:在現有 scope 中啟動新的 Coroutine,回傳 Job,可以用 start()join() 控制執行時機。適合沒有回傳值的非同步任務。
  • async { ... }:與 launch 類似,但回傳 Deferred<T>,呼叫 await() 可以拿到結果。適合有回傳值的非同步任務。
suspend fun showContents() = coroutineScope {
    launch {
        val token = login(userName, password)
        val contents = fetchLatestContents(token)
        withContext(Dispatchers.Main) {
            showContents(contents)
        }
    }
}

suspend function#

suspend 函式(suspend function)用 suspend 關鍵字標記,意思是「這個函式可能會暫停目前的 Coroutine」。

suspend fun loadProfile(): Profile = coroutineScope {
    val basic = async { fetchBasic() }
    val avatar = async { fetchAvatar() }
    Profile(basic.await(), avatar.await())
}

幾條使用規則:

  • suspend 函式只能在另一個 suspend 函式或 CoroutineScope 中呼叫。
  • 在 suspend 函式裡呼叫普通函式沒有限制。
  • 編譯器會把 suspend 函式拆成狀態機,並隱性傳入一個 Continuation

Coroutine 函式庫提供了一些常用的 top-level suspend 函式:

  • delay(timeMs):暫停 Coroutine 一段時間,但不會阻塞執行緒。
  • yield():把 CPU 讓給同一個 Dispatcher 上的其他任務。
  • withContext(context):在當下 scope 中切換 CoroutineContext,例如改用其他 Dispatcher。
  • withTimeout(timeMs):超過時間還沒完成就擲出例外。
  • withTimeoutOrNull(timeMs):超時時回傳 null 而非擲出例外。
  • awaitAll(deferreds):等所有 Deferred 完成,或其中之一失敗。
  • joinAll(jobs):等所有 Job 完成,相當於 jobs.forEach { it.join() }

把「會等待」的動作都寫成 suspend 函式,是 Coroutine 程式的慣例。這讓呼叫者一眼就能看出函式可能會暫停,編譯器也能在背後把狀態機接好。

Dispatcher#

Dispatcher(調度器)決定 Coroutine 跑在哪條執行緒上。Kotlin 內建四種:

  • Dispatchers.Default:CPU 密集任務的預設池,背景運算用。
  • Dispatchers.Main:主執行緒,負責畫面更新(在 Android 或 JavaFX 等 UI 平台才會有實際實作)。
  • Dispatchers.IO:把阻塞型 I/O 任務交給共享執行緒池執行。
  • Dispatchers.Unconfined:不指定執行緒,Coroutine 在呼叫者目前所在的執行緒上開始。

如果建立 CoroutineScope 時沒指定 Dispatcher,預設使用 Dispatchers.Default。需要切換時用 withContext

suspend fun loadAndRender() {
    val data = withContext(Dispatchers.IO) {
        readFromDisk()
    }
    withContext(Dispatchers.Main) {
        render(data)
    }
}

三者怎麼合作#

把三個要素湊在一起看:

  1. 進入點是 CoroutineScope,它決定 Coroutine 的生存範圍與預設的 CoroutineContext
  2. 在 scope 中呼叫 suspend 函式,讓編譯器產生狀態機,描述「等待」的語意。
  3. Dispatcher 把每段執行片段排到合適的執行緒上跑,必要時透過 withContext 切換。

這三個要素的關係,可以用「scope 提供範圍、suspend 描述任務、dispatcher 決定執行緒」來記。後面討論結構化併發時,會看到 scope 透過 Job 把任務取消、例外處理串成一整套機制。

小結#

  • Coroutine 的三大要素是 CoroutineScope、suspend 函式、Dispatcher(調度器)。
  • CoroutineScope 限定 Coroutine 的生存範圍,常用 coroutineScopelaunchasync 三種建立方式。
  • suspend 函式描述「可能暫停」的工作,內建 delayyieldwithContextwithTimeout 等便利函式。
  • Dispatcher 決定 Coroutine 跑在哪條執行緒上,預設是 Dispatchers.Default,常見搭配還有 MainIOUnconfined

原文出處#

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