Coroutine 的一大特色是可以輕鬆切換執行緒,但實際上開發者並不直接挑選執行緒,而是透過 CoroutineDispatcher(調度器)讓 Coroutine 跑在合適的執行緒上。
建立執行緒是一件耗資源的事,所以多半會搭配執行緒池(Thread Pool):池中預先準備好若干執行緒,任務丟進來時,由池子挑一個適合的執行緒執行,避免短時間反覆建立與銷毀。
Kotlin 的 Coroutine 內建四種常用 Dispatcher:
Dispatchers.DefaultDispatchers.MainDispatchers.IODispatchers.Unconfined
Dispatchers.Default#
Default 並不是「主執行緒」,而是「預設背景執行緒池」。它是一個共享的背景執行緒池,池中執行緒數量等於 CPU 內核數量,最少為 2。Default 適合 CPU 密集(CPU-bound)的任務。
當 launch、async 沒有特別指定 Dispatcher,預設就會落到 Dispatchers.Default。
class Day11 {
val scope = CoroutineScope(Job())
fun dispatchersDefault() = scope.launch(Dispatchers.Default) {
println("thread: ${Thread.currentThread().name}")
}
}fun main() = runBlocking {
val day11 = Day11()
day11.dispatchersDefault()
}執行結果:
thread: DefaultDispatcher-worker-1把 Dispatchers.Default 拿掉,輸出仍然會落在 DefaultDispatcher 上。
Dispatchers.IO#
IO 也是使用共享的背景執行緒池,但目標是 I/O 密集(I/O-bound)任務(檔案、網路),所以池中可用的 worker 數量更多。
class Day11 {
val scope = CoroutineScope(Job())
fun dispatchersIO() = scope.launch(Dispatchers.IO) {
println("thread: ${Thread.currentThread().name}")
}
}執行緒數量上限由系統屬性 kotlinx.coroutines.io.parallelism 決定,預設為 64 與 CPU 核心數兩者較大者。
Dispatchers.Unconfined#
Dispatchers.Unconfined 是「不受限制的」調度器:Coroutine 啟動時會在呼叫者所在的執行緒繼續跑,遇到 suspend 點之後再恢復,恢復的位置就由完成那個 suspend 任務的執行緒接手。
fun main() = runBlocking {
launch(Dispatchers.Unconfined) {
println("Unconfined : I'm working in thread ${Thread.currentThread().name}")
delay(500)
println("Unconfined : After delay in thread ${Thread.currentThread().name}")
}
}執行結果:
Unconfined : I'm working in thread main
Unconfined : After delay in thread kotlinx.coroutines.DefaultExecutor啟動時還在 main 執行緒,delay(500) 完成後則切到 kotlinx.coroutines.DefaultExecutor。Unconfined 通常出現在攔截非 suspend API、或是要從阻塞世界呼叫 Coroutine 相關程式時,一般情境下不建議直接使用。
Dispatchers.Main#
真正執行在主執行緒上的是 Dispatchers.Main,常見的用途是更新畫面。
@JvmStatic
public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher不同平台的「主執行緒」定義不同,因此 Main 會透過 ServiceLoader 取得對應實作:
- 在 JS 與 Native 平台上,等同於 Default。
- 在 JVM 上,依環境而定(Android 的 main thread、JavaFx 或 Swing EDT)。
實務上常見搭配 withContext(Dispatchers.Main) 在背景任務完成後切回主執行緒:
launch {
doSomething()
withContext(Dispatchers.Main) {
updateUI()
}
}自建單一執行緒:newSingleThreadContext#
如果需要把 Coroutine 跑在自己建立的單一執行緒上,可以用 newSingleThreadContext("name") 建立一個專屬的 Dispatcher。它會建立一個全新的執行緒,因此用完記得釋放。
小結#
- Coroutine 透過 Dispatcher 來決定底層執行緒,常見內建 Dispatcher 有 Default、Main、IO、Unconfined。
- CPU 密集任務用 Dispatchers.Default、I/O 密集任務用 Dispatchers.IO、更新畫面切到 Dispatchers.Main。
- 需要客製單一執行緒的情境,可以用 newSingleThreadContext 自建。
- Dispatcher 屬於 CoroutineContext 的一部分,可以用
+與其他 Element 組合,或在 launch / withContext 等地方臨時切換。
原文出處#
- 原書/iThome:https://ithelp.ithome.com.tw/articles/10265312