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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值