Kotlin協程-調度器中的精妙實現

kotlin的默認調度器實現其實有兩個,而我們常用的是DefaultScheduler。另一個是CommonPool。

internal actual fun createDefaultDispatcher(): CoroutineDispatcher =
    if (useCoroutinesScheduler) DefaultScheduler else CommonPool

CommonPool也是一個線程池實現。它創建線程池的部分很有意思,

private fun createPool(): ExecutorService {
    if (System.getSecurityManager() != null) return createPlainPool() //沒有SM?用普通線程池
    // Reflection on ForkJoinPool class so that it works on JDK 6 (which is absent there)
    val fjpClass = Try { Class.forName("java.util.concurrent.ForkJoinPool") }
        ?: return createPlainPool() // Fallback to plain thread pool
    // Try to use commonPool unless parallelism was explicitly specified or in debug privatePool mode
    if (!usePrivatePool && requestedParallelism < 0) {
        Try { fjpClass.getMethod("commonPool")?.invoke(null) as? ExecutorService }
            ?.takeIf { isGoodCommonPool(fjpClass, it) }
            ?.let { return it }
    }
    // Try to create private ForkJoinPool instance
    Try { fjpClass.getConstructor(Int::class.java).newInstance(parallelism) as? ExecutorService }
        ?. let { return it }
    // Fallback to plain thread pool
    return createPlainPool()
}

第一行代碼用來判斷SM是否存在,藉以判斷當前平臺是什麼。在Android平臺上,SecurityManager是永遠爲空的,摘自Android官網的說明。

Security managers do not provide a secure environment for executing untrusted code and are unsupported on Android. Untrusted code cannot be safely isolated within a single VM on Android. Application developers can assume that there’s no SecurityManager installed, i.e. System.getSecurityManager() will return null.

於是在安卓上它會走下面的邏輯,用反射的方式去獲取 ForkJoinPool,並且創建線程池。FokrJoinPool特殊的地方是它實現了一種搶佔式的任務調度方式。如果其中一個線程的任務完成了,它會嘗試去偷別的線程的任務,效率比普通線程池高很多。對於普通線程池,考慮極端情況下,4個線程只有一個不停地有任務入隊,只有它有活幹,那麼其他三個都是在磨洋工。

DefaultDispatcher

既然ForkJoinPool這麼屌,Kotlin自然也會有個參考實現。在默認的調度器實現 CoroutineScheduler,Kotlin也實現了一套完整的搶佔任務邏輯。

CoroutineScheduler有兩個主要的私有隊列,

@JvmField
val globalCpuQueue = GlobalQueue()
@JvmField
val globalBlockingQueue = GlobalQueue()

總的來說這兩個都是公有隊列,區別只是一個負責CPU密集,一個負責IO密集。

在公有隊列之外,每個線程自己還有一個私有隊列,

@JvmField
val localQueue: WorkQueue = WorkQueue()

這個私有隊列是每個線程都有的,它主要負責CPU密集型任務。最精妙的地方在這裏,每個線程在找不到任務可做的時候,會去嘗試偷別的任務的任務!任務調度邏輯在 findTask()裏。

fun findTask(scanLocalQueue: Boolean): Task? {
    if (tryAcquireCpuPermit()) return findAnyTask(scanLocalQueue)
    // If we can't acquire a CPU permit -- attempt to find blocking task
    val task = if (scanLocalQueue) {
        localQueue.poll() ?: globalBlockingQueue.removeFirstOrNull()
    } else {
        globalBlockingQueue.removeFirstOrNull()
    }
    return task ?: trySteal(blockingOnly = true)

tryAcquireCpuPermit()的邏輯簡單說就是,首先它會嘗試看看當前是不是佔有CPU控制權,有的話就看公有隊列和私有隊列哪個優先級更高,哪個高就從哪個取任務。此時取的都還是CPU密集型任務。如果沒有CPU使用權,那麼優先看本地隊列(CPU密集型)是不是有任務,沒有的話再去取公有的IO密集型任務。

如果上面的邏輯走下來,都沒拿到任務的話,它就會通過 trySteal() 嘗試去偷別的線程的任務。

偷任務

偷任務的過程無非是個遍歷線程任務隊列的過程,

private fun trySteal(blockingOnly: Boolean): Task? {
    assert { localQueue.size == 0 }
    val created = createdWorkers
    // 0 to await an initialization and 1 to avoid excess stealing on single-core machines
    if (created < 2) {
        return null
    }

    var currentIndex = nextInt(created)
    var minDelay = Long.MAX_VALUE
    repeat(created) {
        ++currentIndex
        if (currentIndex > created) currentIndex = 1
        val worker = workers[currentIndex] //線程池數組
        if (worker !== null && worker !== this) {
            assert { localQueue.size == 0 }
            val stealResult = if (blockingOnly) {
                localQueue.tryStealBlockingFrom(victim = worker.localQueue) //偷它的任務
            } else {
                localQueue.tryStealFrom(victim = worker.localQueue) //偷它的任務
            }
            if (stealResult == TASK_STOLEN) {
                return localQueue.poll()
            } else if (stealResult > 0) {
                minDelay = min(minDelay, stealResult)
            }
        }
    }
    minDelayUntilStealableTaskNs = if (minDelay != Long.MAX_VALUE) minDelay else 0
    return null
}

關鍵的部分已經註釋好了。總的說這個設計是參考了ForkJoinPool的思想,保證不管什麼時候都沒有線程在磨洋工。

從上面的分析可以看出另外一點,在kotlin的協程裏CPU任務和IO任務的優先級是不同的。因爲從CPU使用效率來說,IO任務的CPU使用率遠遠不如CPU密集型任務。它的原因在 Linux內核中斷和io 中有說到。

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