Coroutine 的一大特色是可以輕鬆切換執行緒,但實際上開發者並不直接挑選執行緒,而是透過 CoroutineDispatcher(調度器)讓 Coroutine 跑在合適的執行緒上。

建立執行緒是一件耗資源的事,所以多半會搭配執行緒池(Thread Pool):池中預先準備好若干執行緒,任務丟進來時,由池子挑一個適合的執行緒執行,避免短時間反覆建立與銷毀。

Kotlin 的 Coroutine 內建四種常用 Dispatcher:

  • Dispatchers.Default
  • Dispatchers.Main
  • Dispatchers.IO
  • Dispatchers.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