Kotlin協程-協程派發和調度框架

一個coroutine創建好之後,就交給協程框架去調度了。這篇主要講從launch{…}開始,到最終得到執行的時候,所涉及到的協程框架內部概念。

一般開發中所接觸到的協程類和接口無非是 launch, async, Dispatch.IO…,這些概念是對我們開發者來說的。進入協程源碼的世界之後,這些概念就會被一些內部概念所替代。搞清楚內部概念對分析協程源碼來說非常關鍵。

協程的最小粒度-Coroutine

對沒接觸過協程的人來說,一個OOP代碼的最小調度粒度是函數。在協程中,最小的調度粒度是協程,在kotlin中叫coroutine。

下面是一段協程代碼,

fun main() = runBlocking{
    launch{
        launch {
            // 在後臺啓動一個新的協程並繼續r
            println("launch 3 > Thread: ${Thread.currentThread().name}")
            delay(1000L) // 非阻塞的等待 1 秒鐘(默認時間單位是毫秒)
            println("World!") // 在延遲後打印輸出
        }
        println("launch 2 > Thread: ${Thread.currentThread().name}")
        delay(100L)
        println("Hello,") // 協程已在等待時主線程還在繼續
        Thread.sleep(200L) // 阻塞主線程 2 秒鐘來保證 JVM 存活
    }
    println("out launch done")
}

上面的代碼中總共有三個coroutine。上面的代碼的執行結果是,

out launch done
launch 2 > Thread: main
launch 3 > Thread: main
Hello,
World!

每個協程實際上是個調度單位。
上面的執行順序很有意思,在 “out launch done"結束之後,首先打印了"launch 2”。按理說接下來應該是“Hello”,但實際情況是“launch 3”。

因爲協程在遇到掛起函數delay的時候,會把當前coroutine掛起,然後調度另外一個待執行的coroutine去執行。

在“launch 2”之後,協程遇到了另外一個delay,此時這個coroutine又會被掛起,然後切入之前那個coroutine。所以會看到“launch 2”之後不是“launch 3”,而是Hello。

這個例子是爲了說明coroutine的調度原理。從這個角度看,可以理解“用同步代碼寫異步邏輯”的意思。對於開發者來說,上面的代碼是按同步的思路寫的。實際運行中,因爲coroutine的調度,則變成了異步代碼。

外部概念和內部概念

協程中外部概念和內部概念的差別很大。對應開發者來說,一個協程的最小粒度coroutine,在協程的內部概念中叫DispatchedContinuation。

Continuation是一個內部接口,它有一個對象和一個方法,

public interface Continuation<in T> {
    public val context: CoroutineContext
    public fun resumeWith(result: Result<T>)
}

這個接口的含義是,實現這個接口的類,具有可以暫停和繼續的能力,也就是resumeWith()方法。

在協程源碼裏,所有的coroutine都會被封裝成 DispatchedContinuation對象,但它實際上還是一個coroutine,只是增加了一些能力,這個我們後面會說。

另外還有很多內部概念,比較重要的是 Dispatcher 和 Scheduler。

Dispatcher

顧名思義這是個分發器類。它負責把 DispatchedContinuation 分發到不同的調度器中去執行。

Dispatcher的默認實現是一個 expect 類,

public expect object Dispatchers {
    public val Default: CoroutineDispatcher
    
    public val Main: MainCoroutineDispatcher
    
    public val Unconfined: CoroutineDispatcher
}

它提供了幾個默認的派發邏輯,比如Default會在默認線程池中隨機執行coroutine,Main會在主線程,比如Android中的UI線程裏執行coroutine。還有Unconfined。

在jvm上,還多了個IO類型的默認實現。對於磁盤IO或者網絡IO,一般用這個作爲默認的實現。它對線程池有特殊的調度方式,可以保證計算密集型的coroutine效率不會受到IO coroutine的拖累。

Scheduler

調度器是協程的核心功能,所有的corotuine最後都在Scheduler中執行。

在默認情況下調度器的實現是 CoroutineScheduler,它會根據當前任務數量創建線程,把coroutine放到線程的自有隊列和公有隊列中。並且還實現了 ForkJoinPool 的核心思想 work-stealing。

private fun trySteal(blockingOnly: Boolean): Task? {
    ...
}

work-stealing的核心邏輯是,所有線程先去執行CPU密集型的coroutine。CPU密集型隊列執行完之後,線程再去執行IO密集型coroutine。

最精彩的部分來了,當線程A的IO密集隊列執行完畢,隊列爲空之後,它會去偷其他線程的IO任務隊列。這是整個協程調度裏最精彩的部分,work-stealing的設計,加上把CPU-bounded和IO-intensive任務區分出來,使得用了協程的代碼效率得到極大的提升。

爲什麼可以提升效率,在Kotlin協程-協程設計的基礎中有具體解釋。

協程框架三大件,Continuation-Disptacher-Scheduler

Kotlin的協程從框架設計上就考慮了跨平臺的問題。

這裏的跨平臺不是指安卓和服務端。而是指kotlin在支持 jvm / js/ native 三個平臺上的跨平臺。從它的設計上也能感受到kotlin想吊打其他語言的野心。俗話說“javascript搶別人的活”,現在看kotlin連js的活也想搶。不知道過個十年kotlin和js有哪個還能被開發者使用。

kotlinx-coroutine的代碼框架這麼分,

common
–jvm
–js
–native

在kotlinx-coroutines-core中有個公有的 common 包。除此之外還有js,jvm,native三個包。

協程框架三大件在common裏有通用實現,具體到每個平臺上,還有真正的細節實現。

比如Dispatcher,在 common 包中是 Dispatchers.common.kt,

public expect object Dispatchers {
    public val Default: CoroutineDispatcher
    
    public val Main: MainCoroutineDispatcher
    
    public val Unconfined: CoroutineDispatcher
}

在jvm包中,是Dispatchers

public expect object Dispatchers {
    public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
    ...
}

可以看到jvm包中的方法名多了 actual修飾,後面也有具體的實現createDefaultDispatcher()。這是在java平臺上真正用到的代碼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章