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