Android协程(Coroutines)系列-深入理解dispatchers协程调度

4,066 阅读8分钟

小知识,大挑战!本文正在参与“   程序员必备小知识   ”创作活动

本文同时参与 「掘力星计划」   ,赢取创作大礼包,挑战创作激励金

📚 如果您是 Android 平台上协程的初学者,请查阅上一篇文章: Android协程(Coroutines)系列-入门

🍈 dispatchers简介

协程上下文包含一个 协程调度器 (参见 CoroutineDispatcher)它确定了相关的协程在哪个线程或哪些线程上执行。协程调度器可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。

所有的协程构建器诸如 launch 和 async 接收一个可选的 CoroutineContext 参数,它可以被用来显式的为一个新协程或其它上下文元素指定一个调度器。

Dispatchers源码

public actual object Dispatchers {

    // 默认调度器  
    @JvmStatic
    public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
    // UI调度器  
    @JvmStatic
    public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
    // 无限制调度器  
    @JvmStatic
    public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
    // IO调度器  
    @JvmStatic
    public val IO: CoroutineDispatcher = DefaultScheduler.IO
}

Dispatchers中提供了4种类型调度器:

实现类型具体调度器解释
Default线 程 池DefaultScheduler/CommonPool协程内部实现的Excutor线程池,核心线程数和最大线程数依赖CPU数量,适用于计算类耗时任务调度,比如逻辑计算
MainUI线程MainCoroutineDispatcher通过MainLooper的handler来向主线程派发任务到主线程执行
Unconfined直接执行Unconfined无指定派发线程,会根据运行时的上下文环境决定。
IO线程池LimitingDispatcherIO和Default共享线程池,但运行并发数不同,支持最大并行任务数,适用IO任务调度,比如读写文件,网络请求等

从结构上来讲,Dispatchers的父类是ContinuationInterceptor,然后再继承于CoroutineContext

它们的类结构关系如下:

微信图片_20211025163715.jpg

这也是为什么Dispatchers能加入到CoroutineContext中的原因,并且支持+操作符来完成增加

ContinuationInterceptor(续体拦截器)

// ContinuationInterceptor.kt
@SinceKotlin("1.3")
public interface ContinuationInterceptor : CoroutineContext.Element {

    // 定义上下文拦截器的键
    companion object Key : CoroutineContext.Key<ContinuationInterceptor>

    // 返回原始封装的Continuation,从而拦截所有的恢复
    public fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>

    // 初始化时,为interceptContinuation返回的Continuation实例调用
    public fun releaseInterceptedContinuation(continuation: Continuation<*>) {
        /* do nothing by default */
    }

    public override operator fun <E : CoroutineContext.Element> get(key: CoroutineContext.Key<E>): E? {
        // getPolymorphicKey专门用于ContinuationInterceptor的键
        @OptIn(ExperimentalStdlibApi::class)
        if (key is AbstractCoroutineContextKey<*, *>) {
            @Suppress("UNCHECKED_CAST")
            return if (key.isSubKey(this.key)) key.tryCast(this) as? E else null
        }
        @Suppress("UNCHECKED_CAST")
        return if (ContinuationInterceptor === key) this as E else null
    }


    public override fun minusKey(key: CoroutineContext.Key<*>): CoroutineContext {
        // minusPolymorphicKey专门用于ContinuationInterceptor的键
        @OptIn(ExperimentalStdlibApi::class)
        if (key is AbstractCoroutineContextKey<*, *>) {
            return if (key.isSubKey(this.key) && key.tryCast(this) != null) EmptyCoroutineContext else this
        }
        return if (ContinuationInterceptor === key) EmptyCoroutineContext else this
    }
}

1.拦截器的Key是单例的,因此当你添加多个拦截器时,生效的只会有一个

2.我们都知道,Continuation在调用其Continuation resumeWith()方法,会执行其suspend修饰的函数的代码块,如果我们提前拦截到,是不是可以做点其他事情?这就是调度器切换线程的原理

上面我们已经介绍了是通过Dispatchers指定协程运行的线程,通过interceptContinuation在协程恢复前进行拦截,从而切换线程.

withContext

我们可以调用withContext函数,并且传入相应的协程上下文(CoroutineContext)就可以调度线程。

withContext函数是个suspend函数,它可以在不引用回调的情况下控制任何代码行的线程池,因此可以将其应用于非常小的函数,示例代码如下所示:

suspend fun getUserInfo() {       // Dispatchers.Main
    val data = fetchUserInfo()    // Dispatchers.Main
    show(data)                    // Dispatchers.Main
}

suspend fun fetchUserInfo() {     // Dispatchers.Main
    withContext(Dispatchers.IO) { // Dispatchers.IO
        // 执行网络请求             // Dispatchers.IO
    }                             // Dispatchers.Main
}

在示例代码中,getUserInfo函数在主线程上执行,它可以安全地调用fetchUserInfo函数,在工作线程中执行网络请求,并且挂起,在withContext代码块执行完成后,主线程上的协程就会根据fetchUserInfo函数的结果恢复后面的逻辑。

相比于回调实现,使用withContext函数不会增加额外的开销,在某些情况下,甚至优于回调实现,例如:某个函数执行了很多次的网络请求,使用外部withContext函数可以让Kotlin停留在同一个调度程序,并且只切换一次线程,此外,Kotlin还优化了Dispatchers.Default和Dispatchers.IO之间的切换,以尽可能避免线程切换。

要注意的是,Dispatchers.Default和Dispatchers.IO都是使用线程池的调度程序,它们不能保证代码块在同一线程从上到下执行,因为在某些情况下,Kotlin会在挂起和恢复后,将执行工作移交给另外一个线程,这意味着,对于整个withContext代码块,线程局部变量并不指向同一个值。

Dispatchers.Main源码

// Dispatchers.kt
public actual object Dispatchers {
    // 省略部分代码
    @JvmStatic
    public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
// 省略部分代码

然后看下MainDispatcherLoader.dispatcher,源码如下所示:

// MainDispatchers.kt
internal object MainDispatcherLoader {

    // 省略部分代码

    @JvmField
    val dispatcher: MainCoroutineDispatcher = loadMainDispatcher()

    // 省略部分代码

}

变量dispatcher为MainCoroutineDispatcher类型,MainCoroutineDispatcher是个抽象类,然后它的其中一个实现类是包装类(sealed class)HandlerDispatcher,也就是它的子类肯定是在这个类的所在的文件中,然后我找到HandlerContext这个类,这个类是HandlerDispatcher的唯一子类,源码如下所示:

// MainCoroutineDispatcher.kt
internal class HandlerContext private constructor(
    private val handler: Handler,
    private val name: String?,
    private val invokeImmediately: Boolean
) : HandlerDispatcher(), Delay {
    // HandlerContext的构造函数,参数handler为要传进来的Handler,参数name为用于调试的可选名称
    public constructor(
        handler: Handler,
        name: String? = null
    ) : this(handler, name, false)

    @Volatile
    private var _immediate: HandlerContext? = if (invokeImmediately) this else null

    override val immediate: HandlerContext = _immediate ?:
    HandlerContext(handler, name, true).also { _immediate = it }

    // 判断是否需要调度,参数context为CoroutineContext
    override fun isDispatchNeeded(context: CoroutineContext): Boolean {
        // 判断invokeImmediately的值或者是否是同一个线程
        return !invokeImmediately || Looper.myLooper() != handler.looper
    }

    // 调度线程,参数context为CoroutineContext,参数block为Runnable
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        // 调用Handler的post方法,将Runnable添加到消息队列中,这个Runnable会在这个Handler附加在线程上的时候运行
        handler.post(block)
    }

    override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
        val block = Runnable {
            with(continuation) { resumeUndispatched(Unit) }
        }
        // 调用Handler的postDelayed方法,将Runnable添加到消息队列中,并且在指定的时间结束后运行
        handler.postDelayed(block, timeMillis.coerceAtMost(MAX_DELAY))
        continuation.invokeOnCancellation { handler.removeCallbacks(block) }
    }

    override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle {
        // 调用Handler的postDelayed方法,将Runnable添加到消息队列中,并且在指定的时间结束后运行
        handler.postDelayed(block, timeMillis.coerceAtMost(MAX_DELAY))
        return object : DisposableHandle {
            override fun dispose() {
                // 调用Handler的removeCallbacks方法,删除消息队列中的Runnable
                handler.removeCallbacks(block)
            }
        }
    }

    override fun toString(): String =
        if (name != null) {
            if (invokeImmediately) "$name [immediate]" else name
        } else {
            handler.toString()
        }

    override fun equals(other: Any?): Boolean = other is HandlerContext && other.handler === handler
    override fun hashCode(): Int = System.identityHashCode(handler)
}

然后我们找下调用HandlerContext的构造函数的地方,源码如下所示:

@JvmField
@Deprecated("Use Dispatchers.Main instead", level = DeprecationLevel.HIDDEN)
internal val Main: HandlerDispatcher? = runCatching { HandlerContext(Looper.getMainLooper().asHandler(async = true)) }.getOrNull()

可以看到传入了Looper.getMainLooper方法,也就是应用程序的主循环程序(Main Looper),它位于应用程序的主线程中。

可以看到使用了很多Handler相关的方法,也就是它还是依赖于Android的消息机制。

Dispatchers.Default源码

// Dispatchers.kt
public actual object Dispatchers {

    // 省略部分代码

    @JvmStatic
    public actual val Default: CoroutineDispatcher = createDefaultDispatcher()

    // 省略部分代码

}
// CoroutineContext.kt
internal actual fun createDefaultDispatcher(): CoroutineDispatcher =
    if (useCoroutinesScheduler) DefaultScheduler else CommonPool

这里会根据内部变量(internal val)useCoroutinesScheduler判断返回是DefaultScheduler还是CommonPool,useCoroutinesScheduler源码如下所示:

// CoroutineContext.kt
internal const val COROUTINES_SCHEDULER_PROPERTY_NAME = "kotlinx.coroutines.scheduler"

internal val useCoroutinesScheduler = systemProp(COROUTINES_SCHEDULER_PROPERTY_NAME).let { value ->
    when (value) {
        null, "", "on" -> true
        "off" -> false
        else -> error("System property '$COROUTINES_SCHEDULER_PROPERTY_NAME' has unrecognized value '$value'")
    }
}

这个内部变量(internal val)useCoroutinesScheduler是根据JVM的System.getProperty方法获取的,通过传入 kotlinx.coroutines.scheduler"作为键(key),返回的值为on,useCoroutinesScheduler为true;返回的值是off,useCoroutinesScheduler为false。

先看下DefaultScheduler这种情况,源码如下所示:

// Dispatcher.kt
internal object DefaultScheduler : ExperimentalCoroutineDispatcher() {
    val IO = blocking(systemProp(IO_PARALLELISM_PROPERTY_NAME, 64.coerceAtLeast(AVAILABLE_PROCESSORS)))

    override fun close() {
        throw UnsupportedOperationException("$DEFAULT_SCHEDULER_NAME cannot be closed")
    }

    override fun toString(): String = DEFAULT_SCHEDULER_NAME

    @InternalCoroutinesApi
    @Suppress("UNUSED")
    public fun toDebugString(): String = super.toString()
}

它继承ExperimentalCoroutineDispatcher类,它是个不稳定的类,以后可能会改变,可以看下这个类的dispatch函数,这个函数负责调度线程,源码如下所示:

// Dispatcher.kt
@InternalCoroutinesApi
open class ExperimentalCoroutineDispatcher(
    // 核心线程数
    private val corePoolSize: Int,
    // 最大线程数
    private val maxPoolSize: Int,
    // 调度器保持存活的时间(单位:纳秒)
    private val idleWorkerKeepAliveNs: Long,
    // 调度器名字
    private val schedulerName: String = "CoroutineScheduler"
) : ExecutorCoroutineDispatcher() {
    constructor(
        corePoolSize: Int = CORE_POOL_SIZE,
        maxPoolSize: Int = MAX_POOL_SIZE,
        schedulerName: String = DEFAULT_SCHEDULER_NAME
    ) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS, schedulerName)

    @Deprecated(message = "Binary compatibility for Ktor 1.0-beta", level = DeprecationLevel.HIDDEN)
    constructor(
        corePoolSize: Int = CORE_POOL_SIZE,
        maxPoolSize: Int = MAX_POOL_SIZE
    ) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS)

    // 省略部分代码

    override fun dispatch(context: CoroutineContext, block: Runnable): Unit =
        try {
            // 调用了coroutineScheduler的dispatch函数
            coroutineScheduler.dispatch(block)
        } catch (e: RejectedExecutionException) {
            DefaultExecutor.dispatch(context, block)
        }

    // 省略部分代码

}

看下CoroutineScheduler这个类,然后再看下它的dispatch函数,源码如下所示:

// CoroutineScheduler.kt
@Suppress("NOTHING_TO_INLINE")
internal class CoroutineScheduler(
    // 核心线程数
    @JvmField val corePoolSize: Int,
    // 最大线程数
    @JvmField val maxPoolSize: Int,
    // 调度器保持存活的时间(单位:纳秒)
    @JvmField val idleWorkerKeepAliveNs: Long = IDLE_WORKER_KEEP_ALIVE_NS,
    // 调度器名字
    @JvmField val schedulerName: String = DEFAULT_SCHEDULER_NAME
) : Executor, Closeable {
    init {
        require(corePoolSize >= MIN_SUPPORTED_POOL_SIZE) {
            "Core pool size $corePoolSize should be at least $MIN_SUPPORTED_POOL_SIZE"
        }
        require(maxPoolSize >= corePoolSize) {
            "Max pool size $maxPoolSize should be greater than or equals to core pool size $corePoolSize"
        }
        require(maxPoolSize <= MAX_SUPPORTED_POOL_SIZE) {
            "Max pool size $maxPoolSize should not exceed maximal supported number of threads $MAX_SUPPORTED_POOL_SIZE"
        }
        require(idleWorkerKeepAliveNs > 0) {
            "Idle worker keep alive time $idleWorkerKeepAliveNs must be positive"
        }
    }

    // 省略部分代码

    fun dispatch(block: Runnable, taskContext: TaskContext = NonBlockingContext, tailDispatch: Boolean = false) {
        // 用于支持虚拟时间
        trackTask()
        val task = createTask(block, taskContext)
        // 尝试将任务提交到本地队列,并且根据结果采取执行相关的逻辑
        val currentWorker = currentWorker()
        val notAdded = currentWorker.submitToLocalQueue(task, tailDispatch)
        if (notAdded != null) {
            if (!addToGlobalQueue(notAdded)) {
                // 全局队列在最后一步关闭时不应该接受更多的任务
                throw RejectedExecutionException("$schedulerName was terminated")
            }
        }
        val skipUnpark = tailDispatch && currentWorker != null
        if (task.mode == TASK_NON_BLOCKING) {
            if (skipUnpark) return
            // 执行任务
            signalCpuWork()
        } else {
            // 增加阻塞任务
            signalBlockingWork(skipUnpark = skipUnpark)
        }
    }

    // 省略部分代码

}

可以看到CoroutineScheduler实现了Executor接口,在Java中线程池的核心实现类是ThreadPoolExecutor类,它也是实现了Executor接口,所以这个CoroutineScheduler是协程中线程池的一种实现。

corePoolSize是核心线程数量,它是通过调用JVM的Runtime.getRuntime().availableProcessors()方法得到当前处理器可运行的线程数,它的缺省值强制设置为至少两个线程。

maxPoolSize是最大线程数量,最小值为corePoolSize,最大值为(1 shl BLOCKING_SHIFT) - 2,BLOCKING_SHIFT为21,也就是1向左位移21位再减去2,确保Runtime.getRuntime().availableProcessors()得到的值再乘以2在最小值和最大值之间。

这个函数做的事情就是将传入的任务压入任务栈,然后调用signalCpuWork执行任务或者调用signalBlockingWork来增加阻塞任务。

然后再看下另外一种情况:CommonPool,源码如下所示:

// CommonPool.kt
internal object CommonPool : ExecutorCoroutineDispatcher() {

    // 省略部分代码

    private fun createPool(): ExecutorService {
        if (System.getSecurityManager() != null) return createPlainPool()
        // ForkJoinPool类的反射,方便它在JDK6上可以运行(这里没有),如果没有就使用普通线程池
        val fjpClass = Try { Class.forName("java.util.concurrent.ForkJoinPool") }
            ?: return createPlainPool()
        // 尝试使用commonPool,除非显式指定了并行性或者在调试privatePool模式
        if (!usePrivatePool && requestedParallelism < 0) {
            Try { fjpClass.getMethod("commonPool")?.invoke(null) as? ExecutorService }
                ?.takeIf { isGoodCommonPool(fjpClass, it) }
                ?.let { return it }
        }
        // 尝试创建私有ForkJoinPool实例
        Try { fjpClass.getConstructor(Int::class.java).newInstance(parallelism) as? ExecutorService }
            ?. let { return it }
        // 使用普通线城市
        return createPlainPool()
    }

    // 省略部分代码

    // 创建普通线程池
    private fun createPlainPool(): ExecutorService {
        val threadId = AtomicInteger()
        // 使用Java的newFixedThreadPool线程池
        return Executors.newFixedThreadPool(parallelism) {
            Thread(it, "CommonPool-worker-${threadId.incrementAndGet()}").apply { isDaemon = true }
        }
    }

    // 省略部分代码

    // 调度线程
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        try {
            (pool ?: getOrCreatePoolSync()).execute(wrapTask(block))
        } catch (e: RejectedExecutionException) {
            unTrackTask()
            DefaultExecutor.enqueue(block)
        }
    }

    // 省略部分代码

}

可以看到使用CommonPool,其实就是使用Java的newFixedThreadPool线程池。

Dispatchers.Default调度器的核心线程池和处理器的线程数是相等的,因此它可以用于处理密集型计算,适合在主线程之外执行占用大量CPU资源的工作,例如:对列表排序和解析JSON,和RxJava的计算线程池的思想有点类似。

Dispatchers.IO源码

// Dispatchers.kt
public actual object Dispatchers {
    // 省略部分代码
    @JvmStatic
    public val IO: CoroutineDispatcher = DefaultScheduler.IO
}

可以看到IO其实是DefaultScheduler的一个成员变量,源码如下所示:

internal object DefaultScheduler : ExperimentalCoroutineDispatcher() {
    // 调用了父类ExperimentalCoroutineDispatcher的blocking函数
    val IO = blocking(systemProp(IO_PARALLELISM_PROPERTY_NAME, 64.coerceAtLeast(AVAILABLE_PROCESSORS)))

    override fun close() {
        throw UnsupportedOperationException("$DEFAULT_SCHEDULER_NAME cannot be closed")
    }

    override fun toString(): String = DEFAULT_SCHEDULER_NAME

    @InternalCoroutinesApi
    @Suppress("UNUSED")
    public fun toDebugString(): String = super.toString()
}

可以看下它的父类ExperimentalCoroutineDispatcher的blocking函数,源码如下所示:

// Dispatcher.kt
@InternalCoroutinesApi
open class ExperimentalCoroutineDispatcher(
    // 核心线程数
    private val corePoolSize: Int,
    // 最大线程数
    private val maxPoolSize: Int,
    // 调度器保持存活的时间(单位:纳秒)
    private val idleWorkerKeepAliveNs: Long,
    // 调度器名字
    private val schedulerName: String = "CoroutineScheduler"
) : ExecutorCoroutineDispatcher() {
    constructor(
        corePoolSize: Int = CORE_POOL_SIZE,
        maxPoolSize: Int = MAX_POOL_SIZE,
        schedulerName: String = DEFAULT_SCHEDULER_NAME
    ) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS, schedulerName)

    @Deprecated(message = "Binary compatibility for Ktor 1.0-beta", level = DeprecationLevel.HIDDEN)
    constructor(
        corePoolSize: Int = CORE_POOL_SIZE,
        maxPoolSize: Int = MAX_POOL_SIZE
    ) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS)

    // 省略部分代码

    public fun blocking(parallelism: Int = BLOCKING_DEFAULT_PARALLELISM): CoroutineDispatcher {
        require(parallelism > 0) { "Expected positive parallelism level, but have $parallelism" }
        // 创建LimitingDispatcher对象
        return LimitingDispatcher(this, parallelism, TASK_PROBABLY_BLOCKING)
    }

    // 省略部分代码

}

看下LimitingDispatcher类,源码如下所示:

// Dispatcher.kt
private class LimitingDispatcher(
    // final变量dispatcher为ExperimentalCoroutineDispatcher类型
    val dispatcher: ExperimentalCoroutineDispatcher,
    val parallelism: Int,
    override val taskMode: Int
) : ExecutorCoroutineDispatcher(), TaskContext, Executor {

    // 省略部分代码

    // 调度线程,调用dispatch(block: Runnable, tailDispatch: Boolean)函数
    override fun dispatch(context: CoroutineContext, block: Runnable) = dispatch(block, false)

    private fun dispatch(block: Runnable, tailDispatch: Boolean) {
        var taskToSchedule = block
        while (true) {
            // 提交正在执行的任务槽
            val inFlight = inFlightTasks.incrementAndGet()

            // 快速路径,如果没有达到并行性限制,就会分派任务并且返回
            if (inFlight <= parallelism) {
                // 调用ExperimentalCoroutineDispatcher的dispatchWithContext函数
                dispatcher.dispatchWithContext(taskToSchedule, this, tailDispatch)
                return
            }

            // 达到并行性限制后就将任务添加到队列中
            queue.add(taskToSchedule)

            if (inFlightTasks.decrementAndGet() >= parallelism) {
                return
            }

            taskToSchedule = queue.poll() ?: return
        }
    }

    // 省略部分代码

}

可以看到其实Dispatchers.Default调度器和Dispatchers.IO调度器是共用同一个线程池的。