kotlin协程硬核解读(6. 协程调度器实现原理)

版权声明:本文为openXu原创文章【openXu的博客】,未经博主允许不得以任何形式转载

《kotlin协程硬核解读(4. 协程的创建和启动流程分析)》一文中,我们分析了协程自创建器创建协程对象到开始执行协程代码块中代码的整个过程,文章最后总结了协程有3层包装:

  • 第一层是通过协程构建器创建的AbstractCoroutine子类类型的协程对象,它的作用是维护了协程的上下文
  • 第二层是编译期生成的SuspendLambda的子类对象,封装了协程代码块中的代码和执行逻辑
  • 第三层是DispatchedContinuation,封装了协程的线程调度,也就是决定协程代码块中的代码是在那个线程上执行的

在kotlin协程中,协程的执行涉及到3次线程切换,分别是:

  • 切换到指定线程执行协程代码块中的代码
  • 当协程代码块调用到异步挂起函数时,切换到指定线程执行挂起函数
  • 当异步挂起函数执行完毕,将函数执行结果Result对象切换到协程所在的线程,继续执行协程代码块中剩下的代码

这些线程切换的动作都是通过协程调度器来实现的,本文将对协程调度器做详细讲解。

1. 相关类介绍

1.1 ContinuationInterceptor续体拦截器

《kotlin协程硬核解读(3. suspend挂起函数&挂起和恢复的实现原理)》一文的2.3 SuspendLambda节我们讲过续体Continuation相关的类,其第二层实现类ContinuationImpl(第一层是BaseContinuationImpl)主要就是增加了续体拦截器的功能函数intercepted(),从协程上下文中获取KeyContinuationInterceptor的续体拦截器上下文对象。续体拦截器的作用就是将原始续体对象包装为另一种续体类型的对象,从而增强原续体的功能

//续体拦截器,它是一个协程上下文元素
public interface ContinuationInterceptor : CoroutineContext.Element {
    //续体拦截器的键
    companion object Key : CoroutineContext.Key<ContinuationInterceptor>
	//拦截续体,将原始续体(根据前面文章的分析,原始续体通常就是SuspendLambda的子类对象)转换为另一种续体子类类型
    public fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>
	...
}

1.2 Dispatchers调度器

kotlin协程库中有4种调度器,它们都是CoroutineDispatcher的子类对象,而CoroutineDispatcher又实现了ContinuationInterceptor,所以调度器是通过续体拦截器实现的,每个调度器对象是上下文元素同时又是一个续体拦截器:

//Dispatchers单例对象,它包含4中调度器对象
public actual object Dispatchers {
    //4种调度器,都是CoroutineDispatcher的子类对象,CoroutineDispatcher是通过续体拦截器实现的。actual表示与平台相关,不同平台实现不同
    public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
    public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
    public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
    public val IO: CoroutineDispatcher = DefaultScheduler.IO
}
//协程调度器抽象类,定义了切换线程的相关方法
public abstract class CoroutineDispatcher :
	AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
    public companion object Key : AbstractCoroutineContextKey<ContinuationInterceptor, CoroutineDispatcher>(
        ContinuationInterceptor,  { it as? CoroutineDispatcher })
    
        //注意是final修饰的:所有的调度器(续体拦截器)都将原续体包装为DispatchedContinuation类型
	public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
		DispatchedContinuation(this, continuation)
        
    //四种调度器都分别实现下面两个函数,确保在不同的线程调度
	//判断是否需要切换线程,默认为true,表示需要切换线程,具体的调度器需要根绝情况重写
	public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true
	//调度:切换线程执行block
	public abstract fun dispatch(context: CoroutineContext, block: Runnable)


}

//类关系图
CoroutineContext
	.Element        //上下文元素
		|-- ContinuationInterceptor //续体拦截器:将续体包装为另一种续体类型
				|-- CoroutineDispatcher //协程调度器抽象类:实现了拦截器功能,将原续体对象包装为DispatchedContinuation类型
						//四种调度器,实现了CoroutineDispatcher,不同的平台实现不同
						|-- Dispatchers.Default   //
						|-- Dispatchers.Main      //
						|-- Dispatchers.Unconfined//
						|-- Dispatchers.IO        //

2. 协程的3次线程调度

2.1 第一次线程调度:切换到指定线程开始执行协程代码

《kotlin协程硬核解读(4. 协程的创建和启动流程分析)》一文中的第2.1章节,当使用协程构建器创建一个协程对象时,会在newCoroutineContext(context)函数中检查作用域的上下文中是否存在key为ContinuationInterceptor的元素,如果不存在将会添加一个Dispatchers.Default对象,所以协程的上下文中必定会有一个ContinuationInterceptor的元素,也就是线程调度器,来明确协程代码在哪个线程上执行

//☆ kotlin-stlib.jar 
package kotlin.coroutines.jvm.internal
//续体的实现,增加了拦截功能
internal abstract class ContinuationImpl(
    completion: Continuation<Any?>?, private val _context: CoroutineContext?) : BaseContinuationImpl(completion) {
    public override val context: CoroutineContext
        get() = _context!!
    private var intercepted: Continuation<Any?>? = null   //被拦截的原始续体对象(SuspendLambda子类对象)
    //3.1 从上下文中获取拦截器,实现续体对象包装
    public fun intercepted(): Continuation<Any?> =
        intercepted?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
                .also { intercepted = it }
       ...
}

在启动协程之前,会调用createCoroutineUnintercepted(receiver, completion)创建SuspendLambda子类类型的原始续体对象,然后调用续体的intercepted()将其包装为DispatchedContinuation类型,最后调用resumeCancellableWith()启动协程。resumeCancellableWith()函数中使用调度器对象dispatcher判断是否需要切换线程,如果需要则调用dispatcher.dispatch()实现线程切换

//☆ kotlin-coroutines-core-jvm.jar 
//☆ CoroutineContext.kt
package kotlinx.coroutines
public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
    val combined = coroutineContext + context
    val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined
    //★1. 如果上下文中不存在ContinuationInterceptor类型的元素,添加默认的线程调度器Dispatchers.Default
    return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)
        debug + Dispatchers.Default else debug
}

//☆ Cancellable.kt
package kotlinx.coroutines.intrinsics
//启动协程
internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(
    receiver: R, completion: Continuation<T>, onCancellation: ((cause: Throwable) -> Unit)? = null) =
    runSafely(completion) {
        createCoroutineUnintercepted(receiver, completion)    //★2. 重新创建续体对象(SuspendLambda子类对象)
        	.intercepted()    //★3. 从协程上下文中获取调度器拦截原续体,将其包装为DispatchedContinuation类型
        	.resumeCancellableWith(Result.success(Unit), onCancellation)  //★4. 启动协程
    }

//☆ DispatchedContinuation.kt
package kotlinx.coroutines.internal
internal class DispatchedContinuation<in T>(   //调度器续体包装类
    @JvmField val dispatcher: CoroutineDispatcher,  //持有调度器对象
    @JvmField val continuation: Continuation<T>     //持有原始续体对象
) : DispatchedTask<T>(MODE_UNINITIALIZED), CoroutineStackFrame, Continuation<T> by continuation {
    public fun <T> Continuation<T>.resumeCancellableWith(result: Result<T>, onCancellation: ((cause: Throwable) -> Unit)? = null): Unit = when (this) {
        //★5. 如果续体类型是DispatchedContinuation,调用resumeCancellableWith()启动协程
        is DispatchedContinuation -> resumeCancellableWith(result, onCancellation)
        else -> resumeWith(result)
    }
    inline fun resumeCancellableWith(result: Result<T>, noinline onCancellation: ((cause: Throwable) -> Unit)?) {
        val state = result.toState(onCancellation)
        if (dispatcher.isDispatchNeeded(context)) {   //★6. 判断是否需要切换线程
            _state = state
            resumeMode = MODE_CANCELLABLE
            dispatcher.dispatch(context, this)    //★7. 切换线程后开始执行协程代码
        } else {
            executeUnconfined(state, MODE_CANCELLABLE) {
                if (!resumeCancelled(state)) {
                    resumeUndispatchedWith(result)
                }
            }
        }
    }
}

2.2 第二次线程调度:切换线程执行异步挂起函数

《kotlin协程硬核解读(3. suspend挂起函数&挂起和恢复的实现原理)》一文中讲解了怎样自定义异步挂起函数,要想挂起函数相对于协程异步执行,有两种方式:

//方式1:调用suspendCancellableCoroutine()定义挂起函数,直接在函数中开启Thread,在子线程中执行函数体
suspend fun getUser(): User = suspendCancellableCoroutine {
    continuation ->
    Thread { //也可以通过线程池异步执行,retrofit对协程的支持网络请求就是通过okhttp的异步请求实现的(线程池)
        ...
        continuation.resume(User("openXu"))
    }.start()
}

//方式2:调用withContext(...)定义挂起函数,通过传入调度器上下文实现线程切换
suspend fun getUser(): User = withContext(Dispatchers.IO) {
    ...
    User("openXu")   //子协程返回值
}

第一种方式没什么说的,不管是通过线程池还是Thread,都能将函数体切换到子线程执行。第二种方式withContext(Dispatchers.IO){}当判断传入的调度器和父协程调度器不同时,会创建一个DispatchedCoroutine类型的子协程,函数体就是子协程的挂起代码块,当执行子协程时,又会走2.1的步骤切换到指定线程开始执行子协程代码

public suspend fun <T> withContext(
    context: CoroutineContext,       //协程上下文,一般情况下传递一个调度器
    block: suspend CoroutineScope.() -> T   //子协程代码块
): T {
    ...
    return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
        val oldContext = uCont.context         //父协程的上下文
        val newContext = oldContext + context  //父协程上下文+新的调度器
        ...
        if (newContext === oldContext) {
            //新的上下文和父协程上下文地址相同,不需要切换线程,将创建一个ScopeCoroutine类型的子协程
            val coroutine = ScopeCoroutine(newContext, uCont)
            return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
        }
        // 如果传入的调度器和父协程调度器相同,不需要切换线程,将创建一个UndispatchedCoroutine类型(ScopeCoroutine子类)的子协程
        if (newContext[ContinuationInterceptor] == oldContext[ContinuationInterceptor]) {
            val coroutine = UndispatchedCoroutine(newContext, uCont)
            withCoroutineContext(newContext, null) {
                return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
            }
        }
        //★上面两种不需要切换线程的就没必要跟踪了。当调度器不同时创建一个DispatchedCoroutine类型的子协程,切换到新线程执行子协程代码块(也就是函数体)
        val coroutine = DispatchedCoroutine(newContext, uCont)
        coroutine.initParentJob()
        //★★★ 接2.1的startCoroutineCancellable(),创建子SuspendLambda续体对象,然后包装为DispatchedContinuation类型,最后dispatcher.dispatch()切换到新线程开始执行函数体
        block.startCoroutineCancellable(coroutine, coroutine)   
        coroutine.getResult()
    }
}

2.3第三次线程调度:异步挂起函数执行完毕后将函数执行结果切回协程所在线程并恢复协程执行

执行异步挂起函数时切换到新的协程执行函数有两种方式,当函数执行完毕切回到协程所在线程同样有两种方式。

对于第一种在函数体内通过线程池或者Thread的方式,当函数执行完毕会调用续体continuationresumeWith()方法恢复协程的执行,而continuation是协程被包装后的DispatchedContinuation类型,所以就是调用DispatchedContinuationresumeWith()

第二种通过withContext()创建了一个子协程,根据《kotlin协程硬核解读(3. suspend挂起函数&挂起和恢复的实现原理》一文中<2.5 挂起、恢复实现原理源码解读>,协程代码块的代码会通过状态机被分为多个执行部分放入的SuspendLambda.invokeSuspend()函数中,invokeSuspend()又是在子协程的续体BaseContinuationImpl.resumeWith()中调用的,所以子协程代码块的返回值将作为最后一次调用invokeSuspend()函数的返回值,在BaseContinuationImpl.resumeWith()函数中这个返回值将被传递给子协程的AbstractCoroutine.resumeWith()函数作为子协程的返回值。withContext()创建的子协程DispatchedCoroutine重写了afterResume()函数,该函数中调用父协程的续体的intercepted()函数再次将父协程续体包装为DispatchedContinuation类型,然后调用resumeCancellableWith()切换线程,将挂起函数的结果返回并恢复父协程执行。

//所有协程对象的抽象父类
public abstract class AbstractCoroutine<in T>(protected val parentContext: CoroutineContext, active: Boolean = true) 
	: JobSupport(active), Job, Continuation<T>, CoroutineScope {
    public final override fun resumeWith(result: Result<T>) {
        val state = makeCompletingOnce(result.toState())   //尝试设置当前协程状态为已完成
        if (state === COMPLETING_WAITING_CHILDREN) return
        afterResume(state)
    }
    protected open fun afterResume(state: Any?): Unit = afterCompletion(state) //afterCompletion()是一个空实现,因为普通的协程代码块不需要返回值
}

//withContext()创建的调度器子协程类型
private class DispatchedCoroutine<in T>(
    context: CoroutineContext,   //父协程上下文+新的调度器
    uCont: Continuation<T>       //★ 持有父协程的续体对象,用于将子协程的执行结果切回到父协程的执行线程
) : ScopeCoroutine<T>(context, uCont) {
    ...
    //重写afterResume()函数,因为withContext()函数是有返回值的,子协程代码块的返回值将作为withContext()函数的返回值
    override fun afterResume(state: Any?) {
        // 通过父协程的续体,切换回父协程所在的线程恢复父协程执行
        uCont
        	.intercepted()  //★ 调用父协程的续体的intercepted(),这个函数将从父协程上下文中获取父协程的调度器,并将父协程续体再次包装为DispatchedContinuation类型
        	.resumeCancellableWith(recoverResult(state, uCont))  //恢复父协程执行,并将子协程的返回值传给父协程(也就是withContext()函数的返回值)
    }
}

2.4 小结

通过上面的对协程执行过程中的3次线程切换的源码跟踪,发现协程中的线程切换都会调用DispatchedContinuationresumeWith()或者resumeCancellableWith()函数,通过调度器对象dispatcher.isDispatchNeeded()判断是否需要切换线程,如果需要则调用dispatcher.dispatch()实现线程切换,然后在切换后的线程上执行Runnable调用原续体的resumeWith()从而触发invokeSuspend()启动或恢复协程执行:

//调度器续体,协程的第3层包装(第一层是协程对象,第二层是SuspendLambda)
internal class DispatchedContinuation<in T>(
    @JvmField val dispatcher: CoroutineDispatcher,
    @JvmField val continuation: Continuation<T>  //原续体对象,可能是SuspendLambda,也可能是DispatchedContinuation
) : DispatchedTask<T>(MODE_UNINITIALIZED), CoroutineStackFrame, Continuation<T> by continuation {
	/**
	 * 重写了续体的resumeWith()函数,在原有的恢复协程执行的基础上增加了线程切换功能,该方法会从挂起函数执行线程 切回到 协程调度线程
	 * resumeWith():挂起函数执行完毕后,通过调用resume系列函数恢复函数执行结果或者异常最终都会调用到该方法,恢复协程执行
	 * resumeCancellableWith():通常是启动协程、子协程执行完毕返回结果恢复父协程执行时调用
	 * resumeCancellableWith()和resumeWith()实现差不多,就多了一个是否cancel的判断,这里就不粘贴代码了
	 */
    override fun resumeWith(result: Result<T>) {
        ...
        //★1. 判断是否需要切换线程
        if (dispatcher.isDispatchNeeded(context)) {
            _state = state
            resumeMode = MODE_ATOMIC
            //★2. 调度器切换线程,然后执行this,也就是在切换后的线程上执行下面的run()方法
            dispatcher.dispatch(context, this)
        } else {
        	//不需要切换线程时,直接调用原续体的resumeWith()
            executeUnconfined(state, MODE_ATOMIC) {
                withCoroutineContext(this.context, countOrElement) {
                    continuation.resumeWith(result)
                }
            }
        }
    }
    
    //this就是代理续体
    override val delegate: Continuation<T>
        get() = this
    
    //DispatchedTask实现了Runnable,DispatchedContinuation继承了它,第2步切换线程时传入的this就表示在指定的线程执行run方法(已经切换线程后执行)
    public final override fun run() {
        ...
        val delegate = delegate as DispatchedContinuation<T>
        val continuation = delegate.continuation  //原续体对象
        val context = continuation.context
        val state = takeState() // NOTE: Must take state in any case, even if cancelled
        withCoroutineContext(context, delegate.countOrElement) {
            val exception = getExceptionalResult(state)
            val job = if (exception == null && resumeMode.isCancellableMode) context[Job] else null
            //★3. 在切换后的线程中调用原续体continuation的resumeWith()从而触发invokeSuspend()启动或恢复协程执行
            if (job != null && !job.isActive) {
                val cause = job.getCancellationException()
                cancelCompletedResult(state, cause)
                continuation.resumeWithStackTrace(cause)   //
            } else {
                if (exception != null) {
                    continuation.resumeWithException(exception) //
                } else {
                    continuation.resume(getSuccessfulResult(state))//
                }
            }
        }
        ...
    }
    
}

3. 调度器实现线程切换的原理

3.1 调度器的平台实现

协程库中有一个Dispatchers单例对象,该对象包含协程的4中调度器成员变量,这些变量都是协程调度器CoroutineDispatcher及其子类的对象:

//Dispatchers单例对象,它包含4中调度器对象
public actual object Dispatchers {
    //4种调度器,都是CoroutineDispatcher的子类对象,CoroutineDispatcher是通过续体拦截器实现的。actual表示与平台相关,不同平台实现不同
    public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
    public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
    public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
    public val IO: CoroutineDispatcher = DefaultScheduler.IO
}

注意观察除了IO外,DefaultMainUnconfined都被actual修饰,actual是kotlin的关键字,表示平台相关的实现。我们知道kotlin是一个跨平台的语言,它可以被编译成各种平台的可执行指令,比如Jvm、Android、js、Native、iOS、Linux、Windows等,在不同的平台上实现有些API难免会有差异。比如一般情况下带有UI相关的平台才会强调主线程,主线程专门用于刷新UI不能进行耗时操作导致UI卡顿,而其他平台如普通jvm上不强调主线程(也就不需要主线程调度器),所以对于Main调度器的实现在不同平台上实现是不一样的。如果我们在普通jvm平台程序中使用Dispatchers.Main会抛异常,说缺少具有Main调度器的模块,需要添加提供Main调度器的依赖,比如kotlinx-coroutines-android扩展包

fun main(){
    runBlocking {
        //普通jvm平台没有Main调度器,测试中可以添加kotlinx-coroutines-test.jar依赖
        withContext(Dispatchers.Main){}  
    }
}
运行结果:
Exception in thread "main" java.lang.IllegalStateException: Module with the Main dispatcher is missing. Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android' and ensure it has the same version as 'kotlinx-coroutines-core'
...

其实上面程序报错的根本原因是当前平台环境中没有添加相关的jar包依赖,在程序运行时没有加载到相应的实现类,从而在某些功能函数执行时抛异常。在程序中使用Dispatchers.Main实际上是MainDispatcherLoader单例中的dispatcher变量,它是通过loadMainDispatcher()函数赋值的,这个函数会从当前平台环境中通过ServiceLoader加载所有MainDispatcherFactory主调度器工厂的实现类,这些实现类通常在协程的各个平台的扩展包中,比如Android平台的kotlinx-coroutines-android.jar扩展包中的AndroidDispatcherFactory, 而当前普通jvm测试程序模块并没有添加扩展包依赖(其实添加了也没用,android中特有的内容比如Handler在普通jvm中并不存在),所以使用主调度器Dispatchers.Main时将返回一个MissingMainCoroutineDispatcher对象,当程序运行时执行协程切换线程时调用isDispatchNeeded()dispatch()等函数就会抛异常:

//☆ kotlinx-coroutines-core-jvm.jar
//☆ 调度器单例
package kotlinx.coroutines
public actual object Dispatchers {
    ...
    //主线程调度器对象,是MainCoroutineDispatcher类型的
    public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
}

//☆ 主调度器加载工具类
package kotlinx.coroutines.internal
internal object MainDispatcherLoader {
    //获取系统属性kotlinx.coroutines.fast.service.loader,当前平台上服务快速加载器是否可用
	private val FAST_SERVICE_LOADER_ENABLED = systemProp(FAST_SERVICE_LOADER_PROPERTY_NAME, true)    
	//主调度器对象
    val dispatcher: MainCoroutineDispatcher = loadMainDispatcher()   
	//★★★ 加载主调度器,通过反射加载主调度器工厂创建主调度器对象,如果不存在工厂,则创建错误调度器MissingMainCoroutineDispatcher
    private fun loadMainDispatcher(): MainCoroutineDispatcher {
       val factories = if (FAST_SERVICE_LOADER_ENABLED) {
                /*
                 * 通过反射加载kotlinx.coroutines.android.AndroidDispatcherFactory(kotlinx-coroutines-android.jar)
                 * 或者kotlinx.coroutines.test.internal.TestMainDispatcherFactory(kotlinx-coroutines-test.jar)
                 * 并创建对象放入工厂对象集合中,供下面通过工厂创建主调度器,如果没有依赖对应的jar,集合为空
                 */
                FastServiceLoader.loadMainDispatcherFactory()
            } else {
                //ServiceLoader用于从当前平台环境中加载指定接口或者抽象类的所有实现类,也就是MainDispatcherFactory的实现类
                ServiceLoader.load(
                        MainDispatcherFactory::class.java,
                        MainDispatcherFactory::class.java.classLoader
                ).iterator().asSequence().toList()
            }
         //如果没有加载到主调度器工厂,则创建一个MissingMainCoroutineDispatcher对象返回
         factories.maxBy { it.loadPriority }?.tryCreateDispatcher(factories)
                ?: createMissingDispatcher()
    }
}

//☆ 主调度器丢失的情况下创建的调度器类型,调用该调度器函数时会throw异常
private class MissingMainCoroutineDispatcher(
    private val cause: Throwable?,
    private val errorHint: String? = null
) : MainCoroutineDispatcher(), Delay {
    ...
    override fun isDispatchNeeded(context: CoroutineContext): Boolean =
        missing()  //★抛异常
    override fun dispatch(context: CoroutineContext, block: Runnable) =
        missing()  //★抛异常
    private fun missing(): Nothing {
        if  (cause == null) {
            throwMissingMainDispatcherException()
        } else {
            val message = "Module with the Main dispatcher had failed to initialize" + (errorHint?.let { ". $it" } ?: "")
            throw IllegalStateException(message, cause)
        }
    }
}

//☆ kotlinx-coroutines-android.jar
//☆ 协程的android扩展包中对主调度器工厂的实现
internal class AndroidDispatcherFactory : MainDispatcherFactory {
    override fun createDispatcher(allFactories: List<MainDispatcherFactory>) =
        HandlerContext(Looper.getMainLooper().asHandler(async = true))
    ...
}

3.2 Android平台下的调度器实现

上面说过,协程中线程调度时都会调用CoroutineDispatcherisDispatchNeeded()dispatch()两个函数, isDispatchNeeded()用于判断当前线程和调度器线程是否相同,如果不同则调用dispatch()进行线程切换,然后在切换后的线程执行Runnable(触发原续体的invokeSuspend())。所以研究某种调度器就看它的这两个函数是怎么实现的。

3.2.1 Dispatchers.Default

//☆ kotlinx-coroutines-core-jvm.jar
public actual object Dispatchers {
    /**
     * 所有标准协程构建器如launch、async等创建的协程如果没有指定调度器,将使用默认的Default调度器
     * 它由JVM上的共享线程池提供支持。默认情况下,此调度器使用的最大并行级别等于CPU内核数,但至少为两个;
     * 并行级别X保证在这个调度程序中并行执行的任务不超过X个
     */
    public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
}

//systemProp()调用System.getProperty(propertyName) 获取当前系统平台的某个属性,属性值可通过System.setProperty(key, value)设置
internal val useCoroutinesScheduler = systemProp("kotlinx.coroutines.scheduler").let { value ->
    when (value) {
        null, "", "on" -> true  //jvm平台上该属性值默认为null,所以这里会返回true,表示使用kotlin实现的默认调度器
        "off" -> false
        else -> error("System property '$COROUTINES_SCHEDULER_PROPERTY_NAME' has unrecognized value '$value'")
    }
}
//★ 创建默认调度器,有两种情况:DefaultScheduler(jvm平台默认) 或者 CommonPool
internal actual fun createDefaultDispatcher(): CoroutineDispatcher =
    if (useCoroutinesScheduler) DefaultScheduler else CommonPool

Dispatchers.Default的实现有2种,第一种是DefaultScheduler,还有一种CommonPool。这两种调度器单例对象都是ExecutorCoroutineDispatcher的子类对象,都是基于线程池实现的。当系统属性kotlinx.coroutines.scheduler的值为off时,会使用CommonPool,但是这个系统属性如果不设置的话默认是null,所以DefaultScheduler更常用。

CoroutineDispatcher   //协程调度器
	|--ExecutorCoroutineDispatcher //线程池协程调度器抽象类,维护了一个线程池Executor对象,具体使用什么样的线程池需要实现类创建
	|	   |--ExperimentalCoroutineDispatcher //实验性的协程调度器:根据指定的并发数和最大线程数创建CoroutineScheduler类型的线程池
	|	   |      |--DefaultScheduler   //★ kotlin实现的默认调度器单例对象(任务抢占式提高CPU利用率)
	|	   |
	|	   |
	|	   |--CommonPool                //★ 通用调度器单例对象(任务拆分式提高CPU利用率)
	|	   |
	|	   |--LimitingDispatcher         //限制调度器
	|	   |      |--DefaultScheduler.IO //★ IO调度器单例对象,详见[3.2.4]
CommonPool(java任务拆分式线程池调度器)

CommonPool协程调度器是通过线程池实现的,它没有重写父类的isDispatchNeeded()函数,默认返回true表示需要切换线程。dispatch()将任务Runnable提交到一个ForkJoinPool类型的特殊线程池。ForkJoinPool是在jdk7引入的并发库类,它可以将一个任务拆分为多个“小任务”,把多个“小任务”放到多个处理器核心上并行执行,当多个“小任务”执行完成之后再将这些执行结果合并起来,充分的利用多CPU、多核CPU的优势,从而提高执行效率:(关于ForkJoinPool这里就不深入了,感兴趣的可以去研究java的线程并发)

internal object CommonPool : ExecutorCoroutineDispatcher() {
	...
    private fun getOrCreatePoolSync(): Executor =
        pool ?: createPool().also { pool = it }
    
    //★★★ 创建ForkJoinPool式线程池,它是一种抢占式任务调度的线程池
    private fun createPool(): ExecutorService {
        // 如果不存在java安全管理器(android平台上为null),将调用createPlainPool()创建一个普通线程池
        if (System.getSecurityManager() != null) return createPlainPool()
        // 下面的代码通过反射创建ForkJoinPool
        val fjpClass = Try { Class.forName("java.util.concurrent.ForkJoinPool") }
            ?: return createPlainPool() 
        if (!usePrivatePool && requestedParallelism < 0) {
            Try { fjpClass.getMethod("commonPool")?.invoke(null) as? ExecutorService }
                ?.takeIf { isGoodCommonPool(fjpClass, it) }
                ?.let { return it }
        }
        Try { fjpClass.getConstructor(Int::class.java).newInstance(parallelism) as? ExecutorService }
            ?. let { return it }
        return createPlainPool()
    }
	//创建普通线程池
    private fun createPlainPool(): ExecutorService {
        val threadId = AtomicInteger()
        return Executors.newFixedThreadPool(parallelism) {
            Thread(it, "CommonPool-worker-${threadId.incrementAndGet()}").apply { isDaemon = true }
        }
    }

    //★★★ 没有重写isDispatchNeeded()函数,默认返回true,表示需要切换线程。因为不管之前是什么线程,只要将任务扔到线程池就需要切换线程
    //public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true
    //★★★ 将Runnable扔到线程池中,实现线程切换
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        try {
            (pool ?: getOrCreatePoolSync()).execute(wrapTask(block))
        } catch (e: RejectedExecutionException) {
            unTrackTask()
            DefaultExecutor.enqueue(block)
        }
    }
   ...
}
DefaultScheduler(kotlin实现的任务抢占式线程池调度器)

上面说过在kotlin协程中使用Dispatchers.Default,默认是使用DefaultSchedulerDefaultSchedulerCommonPool的实现是差不多的,都维护了一个线程池,不同的是使用的线程池不一样:

//默认调度器对象,继承ExperimentalCoroutineDispatcher
internal object DefaultScheduler : ExperimentalCoroutineDispatcher()

//实验性的协程调度器:它维护了一个CoroutineScheduler类型的任务抢占式线程池对象
public open class ExperimentalCoroutineDispatcher(private val corePoolSize: Int, private val maxPoolSize: Int,
    private val idleWorkerKeepAliveNs: Long, private val schedulerName: String = "CoroutineScheduler"
) : ExecutorCoroutineDispatcher() {
    public constructor(
        corePoolSize: Int = CORE_POOL_SIZE,  //线程并发数,默认为2
        maxPoolSize: Int = MAX_POOL_SIZE,    //线程池中最大线程数
        schedulerName: String = DEFAULT_SCHEDULER_NAME) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS, schedulerName)
    
    //★★★ 根据设置的线程并发数和最大数创建一个CoroutineScheduler类型的线程池,源码在下面
    private var coroutineScheduler = createScheduler()  
    private fun createScheduler() = CoroutineScheduler(corePoolSize, maxPoolSize, idleWorkerKeepAliveNs, schedulerName)

    //★★★ 没有重写isDispatchNeeded()函数,默认返回true,表示需要切换线程。因为不管之前是什么线程,只要将任务扔到线程池就需要切换线程
    //public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true
    //★★★ 将Runnable扔到线程池中,实现线程切换
    override fun dispatch(context: CoroutineContext, block: Runnable): Unit =
        try {
            coroutineScheduler.dispatch(block)
        } catch (e: RejectedExecutionException) {
            // CoroutineScheduler only rejects execution when it is being closed and this behavior is reserved
            // for testing purposes, so we don't have to worry about cancelling the affected Job here.
            DefaultExecutor.dispatch(context, block)
        }
	...
}

CommonPool中采用的是java中的ForkJoinPool,这种线程池可以将一个大任务分为多个小任务,交给多个线程执行处理,利用多核CPU优势提高线程池中的线程利用率。kotlin借鉴ForkJoinPool的思想实现了一种抢占式线程池CoroutineScheduler,这个线程池中的Worker类型的线程当任务执行完毕后,会主动去全局任务队列、私有任务队列、其他线程任务队列中寻找任务,避免当前线程休眠,减少线程切换次数,从而提高CPU利用率,不把资源浪费在切换线程这种无意义的事情上:

//CoroutineScheduler线程池,该线程池中的线程都是Worker类型的,具有抢占任务的特性
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 {
	//线程池全局任务队列,这些队列中的任务在执行时都可能涉及到线程池的线程切换
    val globalCpuQueue = GlobalQueue()     //存放CPU密集型任务的队列
    val globalBlockingQueue = GlobalQueue()//存放IO密集型任务的队列
    
    //线程数组
    val workers = AtomicReferenceArray<Worker?>(maxPoolSize + 1)
    
    //★★★ 特殊的线程,可以抢别的线程的任务
    internal inner class Worker private constructor() : Thread() {
      	//每个线程都有一个队列,它主要负责CPU密集型任务
        val localQueue: WorkQueue = WorkQueue() 
        
        override fun run() = runWorker()
        private fun runWorker() {
            var rescanned = false
            while (!isTerminated && state != WorkerState.TERMINATED) {
                //主动寻找任务
                val task = findTask(mayHaveLocalTasks)
                // 找到任务后执行任务
                ...
            }
        }
        /**
         * ★★★ fundTask():当线程的任务执行完毕后,让它主动去寻找任务,从而避免当前线程休眠,减少线程切换次数,以提高CPU利用率
         * 寻找任务
         */
        fun findTask(scanLocalQueue: Boolean): Task? {
            /**
             * tryAcquireCpuPermit()判断当前是否有CPU控制权
             * ★ 如果有CPU控制权:findAnyTask()尝试从全局任务队列中获取任务,意义就是当前线程的任务执行完后主动去全局任务队列找任务,而不是休眠,从而减少一次线程切换,提高CPU利用率
             */
            if (tryAcquireCpuPermit()) return findAnyTask(scanLocalQueue)
            //★ 如果不能获得CPU控制权,尝试从当前线程的任务队列中获取任务,其目的就是阻塞当前线程,不让它休息,使其饥饿式的寻找任务
            val task = if (scanLocalQueue) {
                localQueue.poll() ?: globalBlockingQueue.removeFirstOrNull()
            } else {
                globalBlockingQueue.removeFirstOrNull()
            }
            //如果上面没有拿到任务,通过trySteal()尝试偷别的线程的任务
            return task ?: trySteal(blockingOnly = true)
        }
        
        //★ 偷其他线程的任务
		private fun trySteal(blockingOnly: Boolean): Task? {
            assert { localQueue.size == 0 }
            ...
            repeat(created) {
                ++currentIndex
                if (currentIndex > created) currentIndex = 1
                val worker = workers[currentIndex]   //遍历线程池的线程数组,worker就是一个线程Thread
                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
        }
    }
    
}

3.2.2 Dispatchers.Main

Android中的主调度器是通过主线程Handler实现线程切换的,isDispatchNeeded()中通过判断当前线程的Looper是否等于MainLooper,如果不相等则表示当前线程是子线程,而我们要调度到主线程,所以dispatch()使用Handler将任务提交到主线程消息队列,从而在主线程触发invokeSuspend()

//☆ kotlinx-coroutines-android.jar
//☆ 协程的android扩展包中对主调度器工厂的实现
internal class AndroidDispatcherFactory : MainDispatcherFactory {
    //Android平台上创建HandlerContext类型的主调度器,传入的参数是通过主线程Looper创建的Handler对象
    override fun createDispatcher(allFactories: List<MainDispatcherFactory>) =
        HandlerContext(Looper.getMainLooper().asHandler(async = true))
}

//☆ Android平台UI线程调度器的实现
internal class HandlerContext private constructor(
    private val handler: Handler,   //主线程Handler
    private val name: String?,
    private val invokeImmediately: Boolean
) : HandlerDispatcher(), Delay {
    public constructor(handler: Handler, name: String? = null) : this(handler, name, false)
    //是否需要切换线程?
    override fun isDispatchNeeded(context: CoroutineContext): Boolean {
        //★★★ 当前线程的Looper不等于主线程Looper时,说明当前线程是子线程,需要切换到主调度器指定的主线程中
        return !invokeImmediately || Looper.myLooper() != handler.looper
    }
	//从子线程切换到主线程
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        //★★★ 通过主线程的handler将任务Runnable提交到主线程消息队列,Runnable将在主线程执行
        handler.post(block)
    }
}

3.2.3 Dispatchers.Unconfined

Dispatchers.Unconfined单例对象重写了isDispatchNeeded()直接返回false,表示任何情况下都不需要切换线程,直接在当前线程执行

//☆ kotlinx-coroutines-core-jvm.jar
//不局限于任何特定线程的协程调度器,不需要切换线程
internal object Unconfined : CoroutineDispatcher() {
    //★★★ 不需要切换线程
    override fun isDispatchNeeded(context: CoroutineContext): Boolean = false
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        // It can only be called by the "yield" function. See also code of "yield" function.
        val yieldContext = context[YieldContext]
        if (yieldContext != null) {
            // report to "yield" that it is an unconfined dispatcher and don't call "block.run()"
            yieldContext.dispatcherWasUnconfined = true
            return
        }
        throw UnsupportedOperationException("Dispatchers.Unconfined.dispatch function can only be used by the yield function. " +
            "If you wrap Unconfined dispatcher in your code, make sure you properly delegate " +
            "isDispatchNeeded and dispatch calls.")
    }
}

如果协程构建器传入Dispatchers.Unconfined,将在当前线程启动执行协程,但仅仅只是运行到第一个挂起点(初始状态),第一个挂起点之后的代码在哪个线程上执行将由被调用的挂起函数来决定(挂起函数在哪个线程调度,协程恢复后将在哪个线程执行):

fun main() = runBlocking{
    launch(Dispatchers.Unconfined) { // Dispatchers.Unconfined在当前线程上启动执行协程
        println("Unconfined: 在 ${Thread.currentThread().name}线程执行")     //在main线程执行
        delay(500)   //挂起函数,会在子线程等待延迟0.5s
        println("Unconfined: 延迟后在${Thread.currentThread().name}线程执行") //延迟后在kotlinx.coroutines.DefaultExecutor线程执行
    }
    launch { //继承runBlocking父协程的上下文
        println("main runBlocking: 在 ${Thread.currentThread().name}线程执行")  //在main线程执行
        delay(1000)
        println("main runBlocking: 延迟后在${Thread.currentThread().name}线程执行") //延迟后在main线程执行
    }
}

它是非受限的调度器,非常适用于执行不消耗CPU时间的任务(因为不需要切换线程),以及不更新局限于特定线程的任何共享数据(如UI)的协程,它是runBlocking{} 协程的默认调度器。非受限的调度器是一种高级机制,可以在某些极端情况下提供帮助而不需要调度协程以便稍后执行或产生不希望的副作用, 因为某些操作必须立即在协程中执行。 非受限调度器不应该在通常的代码中使用,也就是说在实际开发中通常不用,只在测试程序中使用

3.2.4 Dispatchers.IO

Dispatchers.IO调度器和Dispatchers.DefaultDefaultScheduler是差不多的,都使用kotlin实现的任务抢占式的CoroutineScheduler类型的线程池。不同的是这两种协程调度器的使用场景不同,Dispatchers.Default适用于CPU密集型任务,Dispatchers.IO适用于I/O密集型任务。

在使用线程池调度器的时候,我们是该选择Dispatchers.Default还是Dispatchers.IO呢?其实只需要区分这个任务是CPU密集型的还是I/O密集型的。

  • CPU密集型

    任何任务在执行时都会消耗CPU和内存,如果任务大部分时间都在做一些计算和逻辑判断等消耗CPU的动作,而I/O在很短的时间就可以完成的任务通常称为CPU密集型任务。这类任务有一个明显的特点,就是在执行时会让CPU占用量瞬间升高,并且维持时间较长(可通过任务管理器的性能查看)。在Android中,加载布局树(递归迭代填充)、识别二维码等任务就属于该类任务。

  • I/O密集型

    I/O密集型任务是相对于CPU密集型而言的,这类任务通常占用较多的内存,而计算相对简单。Android中数据存储、文件拷贝、请求数据等等都属于I/O密集型任务。

不管是什么类型的任务,都是通过线程池中的线程执行的,而且IO调度器和默认调度器使用的是同一种类型的线程池(都是任务抢占式的Worker线程),岂不是没有什么区别了?为什么还要为Dispatchers.IO定义一个LimitingDispatcher实现类呢?原因就是在创建线程池对象时,需要设置该线程池最大线程数和并发数,我们要根据不同类型任务配置不同的并发和最大线程数量,以最大限度提高CPU利用率。

多线程编程中一般最大线程个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完时会保存自己的状态并会重新处于就绪状态让给其他线程使用,而其他线程时间片用完它又有机会争取到执行权,接着之前的状态继续执行。线程切换是计算密集型的,过多的线程切换意味着消耗大量的CPU时间,造成额外的性能开销浪费。而最大并发数也不是随便设置的,最大并发数肯定不能超多CPU核心数,那就等于核心数?如果等于核心数就会导致在某一段时间都在执行某一种类型的任务,而其他任务就被搁置了,造成资源分配不均,从某种意义上来说也是执行效率不高的表现。那到底应该怎样配置线程池呢?其实并没有一个强硬的规定,但是通常会有相应的规则(并不是一定的,网上也有其他规则):

CPU 密集型任务(N+1): 这种任务主要消耗CPU资源,可以将线程最大数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。

I/O 密集型任务(2N): 对于这种任务,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

比如kotlin协程对于Dispatchers.Default调度器的线程池:

  • 并发数量CORE_POOL_SIZE = CPU核心数(最小为2) ,可通过kotlinx.coroutines.scheduler.core.pool.size设置;

  • 最大数量MAX_POOL_SIZE = CPU核心数*128,可通过kotlinx.coroutines.scheduler.max.pool.size设置。不是说N+1吗?为什么最大数量是N*128?其实是因为kotlin协程调度器的线程池中的线程是一种任务抢夺式的Worker线程,上面我们也对其进行分析了,并不会因为线程池最大线程量过大而造成过多的线程切换开销。

Dispatchers.IO调度器的线程池的配置和Dispatchers.Default是相同的,不过在IO调度器中通过parallelism来控制了往线程池同时扔任务的最大数量,如果超过了这个阈值,则先放入到队列中,等待线程池有一个线程空闲时再往里扔,虽然我现在也不知道这么干的意义是什么😀,但是他和默认调度器确实是有区别的。parallelism的默认值 = max(64, jvm虚拟机可用处理器数),可通过kotlinx.coroutines.io.parallelism设置。下面是IO调度器源码:

//☆ kotlinx-coroutines-core-jvm.jar
//IO调度器
public val IO: CoroutineDispatcher = DefaultScheduler.IO

//3.2.1中讲解的默认调度器DefaultScheduler
internal object DefaultScheduler : ExperimentalCoroutineDispatcher() {
    val IO: CoroutineDispatcher = LimitingDispatcher(
        this,  //★传入this,也就是ExperimentalCoroutineDispatcher,所以IO调度器也是使用的任务抢占式的CoroutineScheduler类型线程池
        //从系统配置属性中获取IO密集型任务适用的最大线程数,默认值是 64 和 jvm虚拟机可用的处理器数 中的较大值
        systemProp(IO_PARALLELISM_PROPERTY_NAME, 64.coerceAtLeast(AVAILABLE_PROCESSORS)), 
        "Dispatchers.IO",   //调度器名称
        TASK_PROBABLY_BLOCKING
    )
	...
}

//☆ IO调度器的实现
private class LimitingDispatcher(
    private val dispatcher: ExperimentalCoroutineDispatcher,  //线程池调度器
    private val parallelism: Int,  //同时执行的最大任务数(不是线程池的并发数)
    private val name: String?,
    override val taskMode: Int
) : ExecutorCoroutineDispatcher(), TaskContext, Executor {

    ...
    //★★★ 没有重写isDispatchNeeded()函数,默认返回true,表示需要切换线程。因为不管之前是什么线程,只要将任务扔到线程池就需要切换线程
    //public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true
    
    override fun dispatch(context: CoroutineContext, block: Runnable) = dispatch(block, false)
    //★★★ 将任务扔到任务抢占式CoroutineScheduler类型的线程池中
    private fun dispatch(block: Runnable, tailDispatch: Boolean) {
        var taskToSchedule = block
        while (true) {
            val inFlight = inFlightTasks.incrementAndGet()
            //如果未达到并发数量限制,则立即调度任务并返回
            if (inFlight <= parallelism) {
                dispatcher.dispatchWithContext(taskToSchedule, this, tailDispatch)
                return
            }
            // 已达到并发数量限制,将任务添加到队列
            queue.add(taskToSchedule)
            if (inFlightTasks.decrementAndGet() >= parallelism) {
                return
            }
            taskToSchedule = queue.poll() ?: return
        }
    }
}

3.3 总结

kotlin协程库中的4种调度器的实现是与系统平台相关的:

  • Dispatchers.Main:在带有UI模块的系统中,表示在UI线程调度
  • Dispatchers.Default:通常情况下会采用CoroutineScheduler类型的线程池,这个线程池使用特殊的任务抢占式Worker类型的线程,避免线程切换造成CPU资源消耗。但是如果设置了系统属性kotlinx.coroutines.scheduler的值为off时,将采用java的ForkJoinPool线程池,它是一种任务拆分式线程池,可以将一个大任务拆分为多个小任务后给多个线程并发执行,然后汇总结果,从而提高了CPU利用率。
  • Dispatchers.IO:它和Dispatchers.Default一样采用的是CoroutineScheduler任务抢夺式线程池,区别是在往线程池扔任务之前多了一个队列,用于控制最大并发任务数量
  • Dispatchers.Unconfined:无限制的,在任何情况下都不需要切换线程,直接在当前线程执行,如果异步挂起函数执行完毕后恢复协程执行,协程将沿用挂起函数的线程上执行

希望本文能给各位带来帮助,喜欢的同学不要忘了一键三联(点赞投币+关注)

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

open-Xu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值