iT邦幫忙

2021 iThome 鐵人賽

DAY 11
0
Software Development

Coroutine 停看聽系列 第 11

Day11:調度器(Dispatchers),我跳進來了,又跳出去了

Coroutine 一個重要的特性就是可以輕易的切換執行緒,不過 Coroutine 是使用 CoroutineDispatcher (調度器) 來切換執行緒,而不是直接讓使用者選擇不同的執行緒。因為建立執行緒是一項消耗資源的事,所以通常會建立一個執行緒池 (Thread Pool),在執行緒池中會有多個執行緒在池中等待,當任務丟進執行緒池中執行時,它會選擇適當的執行緒來運行。避免在短時間內建立及銷毀執行緒,造成系統的資源浪費,影響整體效能。當然除了使用執行緒池外,執行緒也可以讓我們直接建立。

不管是執行緒池或者是單獨的執行緒,在 Coroutine 都能夠有相對應的方法支持。

Dispatchers 有分為四種:

Dispatchers.Default

Dispatchers.Main

Dispatchers.IO

Dispatchers.Unconfined


Dispatchers.Default

由於筆者是一名 Android 工程師,第一次見到 Dispatchers.Default 的時候,我以為是使用預設的執行緒,也就是主執行緒,結果跟我想的不一樣。

我們知道 Coroutine 是用來處理非同步的執行,所以當需要使用 Coroutine 的時候,需要把耗時的工作丟到主執行緒以外的地方執行,所以不會是我一開始想的主執行緒。

那麼,使用 Dispatchers.Default 會使用什麼執行緒呢?

前面提到會建立一個執行緒池來執行耗時任務,而 Dispatchers.Default 就是使用背景的共享執行緒池來執行。在這個池中,執行緒的數量會等於 CPU 內核的數量,不過最少會是 2。

使用 launch、async 建立 Coroutine 時,如果沒有特別指定,就會使用 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
  • launch 沒有帶入 Dispatchers.Default 結果也會是一樣。

Dispatchers.IO

如果直接將 Dispatchers.IO 帶入 launch 中,我們會得到下面的結果

class Day11 {
    val scope = CoroutineScope(Job())
		...
    fun dispatchersIO() = scope.launch(Dispatchers.IO) {
        println("thread: ${Thread.currentThread().name}")
    }

}
thread: DefaultDispatcher-worker-1

原因是 Dispatchers.IO 一樣是使用共享的背景執行緒池,但是由於 Dispatcher.IO 目的是提供給 IO 做使用的,所以它所建立的 worker 也會比較多 (worker 其實就是執行緒)

The number of threads used by tasks in this dispatcher is limited by the value of "kotlinx.coroutines.io.parallelism" (IO_PARALLELISM_PROPERTY_NAME) system property.It defaults to the limit of 64 threads or the number of cores (whichever is larger).

在原始碼中的註解告訴我們了,它會建立 64 個執行緒或是 CPU 核心的數量 (看誰比較大)。

Dispatchers.Unconfined

Unconfined 的意思是不受限制的,什麼是不受限制的調度器呢?我們看一下下面這段程式碼:

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

在這一段 coroutine 中,一開始的執行緒是 main ,因為從 runBlocking 呼叫起來的時候是 Dispatchers.Main,當執行到 delay(500) 的時候,這個 coroutine 被暫停,等到 500 毫秒結束之後,就切換至 kotlinx.coroutines.DefaultExecutor 執行緒。

通常用在攔截在非 suspend API 或從阻塞的世界中調用 coroutine 相關的程式。 Ref

Dispatchers.Main

最後是我們的 Dispatchers.Main,還記得在文章最前面提到我搞錯 Dispatchers.Default 是執行在主執行緒上嗎?真正執行在主執行緒上的是 Dispatchers.Main。

在做完耗時的任務之後,最後如果需要更新畫面,我們就必須要把執行緒切回主執行緒上。

@JvmStatic
public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher

因為在不同平台上,主執行緒的使用都各有不同,所以在這邊會根據不同平台來取得相對應的 dispatchers。

On JS and Native it is equivalent of Default dispatcher.
On JVM it is either Android main thread dispatcher, JavaFx or Swing EDT dispatcher. It is chosen by ServiceLoader.

我們可以使用 withContext 在 coroutine 中把執行緒切回主執行緒,如下:

launch {
    doSomething()
        withContext(Dispatchers.Main){
            updateUI()
        }
}

小結

Coroutine 在背景其實還是使用執行緒在做耗時任務,只不過 Coroutine 能夠讓非同步的程式碼更容易寫,正確使用 Dispatchers 能夠讓我們的 coroutine 執行在正確的執行緒上。

參考資料

Kotlin Coroutines Dispatchers 那一兩件事
Coroutine context and dispatchers | Kotlin
CoroutineDispatcher

特別感謝

Kotlin Taiwan User Group
Kotlin 讀書會


上一篇
Day10:例外處理,留下來或我跟你走
下一篇
Day12:內建的 suspend 函式,好函式不用嗎?(1)
系列文
Coroutine 停看聽30

尚未有邦友留言

立即登入留言