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 中有說到。