走完前幾節之後,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)
}
}三者怎麼合作#
把三個要素湊在一起看:
- 進入點是 CoroutineScope,它決定 Coroutine 的生存範圍與預設的
CoroutineContext。 - 在 scope 中呼叫 suspend 函式,讓編譯器產生狀態機,描述「等待」的語意。
- Dispatcher 把每段執行片段排到合適的執行緒上跑,必要時透過
withContext切換。
這三個要素的關係,可以用「scope 提供範圍、suspend 描述任務、dispatcher 決定執行緒」來記。後面討論結構化併發時,會看到 scope 透過
Job把任務取消、例外處理串成一整套機制。
小結#
- Coroutine 的三大要素是 CoroutineScope、suspend 函式、Dispatcher(調度器)。
- CoroutineScope 限定 Coroutine 的生存範圍,常用
coroutineScope、launch、async三種建立方式。 - suspend 函式描述「可能暫停」的工作,內建
delay、yield、withContext、withTimeout等便利函式。 - Dispatcher 決定 Coroutine 跑在哪條執行緒上,預設是
Dispatchers.Default,常見搭配還有Main、IO、Unconfined。
原文出處#
- 原書/iThome:https://ithelp.ithome.com.tw/articles/10262562