协程如何切换线程

在讲解如何切换线程之前,有必要先了解下CoroutineScope。协程作用域,它里面持有CoroutineContext,用来管理它的作用域范围内的所有协程。

CoroutineScope(协程作用域)

CoroutineScope组成

CoroutineScope 可见它只是一个接口,里面定义了一个coroutineContext。前面介绍过coroutineContext是协程的上下文,它里面是由Element拼接而成的,而Element主要分为如下几种:

  • CoroutineDispatcher:它是线程分发器。其中Dispatcher.IO和Dispatcher.Main、Dispatchers.Main.immediate都是继承自于它
  • Job:它是用来取消协程、父协程与子协程取得联系的桥梁。常见的有SupervisorJob
  • CoroutineName:用来给协程起名字的作用
  • CoroutineExceptionHandler:它是用来捕捉协程异常的回调器

所以完整的CoroutineContext组成部分如下:
job+dispatchers+CoroutineName+CoroutineExceptionHandler

CoroutineScope分类

目前kotlin已经内置了各种CoroutineScope,下面来说下他们的区别

GlobalScope

上面用到的GlobalScope就是一个协程作用域: GlobalScope编译后的class 可以看出来它是一个单例的scope,生命周期和app保持一致,并且它的context是一个EmptyCoroutineContext。平时开发时候不会去用它

runBlocking

是一个阻塞式的协程作用域,会阻塞协程外面的线程,内部是通过java的LockSupport.park阻塞住线程来实现的。 runBlocking事例 runBlocking事例日志

MainScope

它是一个指定协程分发在主线程中,并且使用到的Job是SupervisorJob,它的作用是当子协程出错的时候,不影响到父协程中的其他子协程 MainScope 它是一个ContextScope,继承自CoroutineScope,并且它的context是由SupervisorJob()和Dispatchers.Main组成的一个context。我们一般自定义CoroutineScope的时候也是定义一个ContextScope,他的构造方法需要传入一个CoroutineContext。 ContextScope

CoroutineScope顶级方法

CoroutineScope顶级方法 它也是一个ContextScope,它的上下文先看传进来的context中的job是否为空,如果为空,则初始化一个job。如果不为空,则使用传进来的context。

coroutineScope顶级方法

coroutineScope顶级 它是一个suspend方法,它是通过闭包的形式返回一个CoroutineScope,对应的字节码如下: coroutineScope顶级方法对应的字节码 会将ScopeCoroutine给到block,也就是闭包中的CoroutineScope,接着会调用UndispatchedKt.startUndispatchedOrReturn: UndispatchedKt.startUndispatchedOrReturn的调用 最终调用了block.invoke方法,写个demo看下效果: coroutineScope使用
日志如下: coroutineScope使用的日志 从日志可以看出来,它是先等到coroutineScope里面delay执行完了,才执行外面的逻辑。在字节码层面,它会把coroutineScope外面的代码编译成SuspendLambda,它也是个Continuation,等到执行完了coroutineScope内部的代码,才会回调到coroutineScope外面的SuspendLambda中来: coroutineScope事例编译后的字节码 上面分析过UndispatchedKt.startUndispatchedOrReturn中调用了block的invoke方法,此处的block正对应了CoroutineDispatchersActivity$demo7$1这个SuspendLambda对象: demo7中的continuation传递 在demo7中将CoroutineDispatchersActivity$demo7$1这个continuation传递到coroutineScope中,而它的invoke方法如下: coroutineScope事例编译后的字节码 创建了CoroutineDispatchersActivity$demo7$1后,继而调用了invokeSuspend方法,而invokeSuspend里面是要等到coroutineScope闭包中的delay挂起结束后,才会再次回到invokeSuspend方法,最后才会输出「demo7: coroutineScope outside」的日志,这也是协程挂起的调用顺序。

supervisorScope顶级方法

它和上面的coroutineScope顶级方法差不多,也是先调用完闭包中的逻辑,然后才执行supervisorScope外面的代码。看下它的实现就知道区别了: supervisorScope实现 他所使用的CoroutineScope是一个SupervisorCoroutine,它和上面用到的ScopeCoroutine区别是重写了childCancelled方法,并返回false,此方法是子协程发生异常后,该不该取消其它的子协程,下面来验证下: supervisorScope中子协程发生异常 coroutineScope子协程发生异常 在supervisorScope闭包中launch1发生异常了,由于supervisorScope不会去处理异常,将异常交给了launch1处理,所以launch2中的代码能继续执行。而在coroutineScope中,当launch1发生异常的时候,会将异常交给了coroutineScope,最终导致launch2的协程无法继续执行。

Job

Job实现了CoroutineContext.Element,可以用来取消、启动一个协程,并且和父协程绑定了关系: Job结构图 它下面有几个关键的子类: Job的关键子类 从图上可以看出来前面分析的coroutineScope和supervisorScope两个顶级方法所使用的作用域ScopeCoroutine最终也是一个Job。

线程例子

先看一个例子: 例子

分别指定了四种线程的用法,日志如下:
第一次:

第二次:

Dispatchers.Main.immediate:指定的主线程会最先执行
Dispatchers.Main:指定的主线程会晚于协程外面的主线程
Dispatchers.IO和Dispatchers.Default:指定的线程没有先后之分

CoroutineScope.lanuch

lanuch方法是协程作用域的扩展方法,比如上面例子中的GlobalScope它就是继承自CoroutineScope:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    //①
    val newContext = newCoroutineContext(context)
    //②
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    //③
    coroutine.start(start, coroutine, block)
    //④
    return coroutine
}
  • 参数:
    • context:launc传进来的context,比如上面例子中传进来的Dispatchers.Main、Dispatchers.IO这些都是CoroutineContext。如果没传就用EmptyCoroutineContext。
    • start:CoroutineStart的枚举类,表示启动模式,默认是DEFAULT
    • block:协程代码块,是一个CoroutineScope的扩展挂起函数
  1. 将外界传进来的context和当前CoroutineScope的context进行合并处理
  2. 默认是DEFAULT模式,所以会初始化一个StandaloneCoroutine,会把newContext作为自己的parentContext,它既是一个Continuation,也是一个CoroutineScope
  3. 调用StandaloneCoroutine的start方法,并把启动模式,StandaloneCoroutine,协程挂起函数传进去了
  4. StandaloneCoroutine作为lauch的返回值

StandaloneCoroutine它是一个Job类型,也是继承自AbstractCoroutine,看下它的start方法: AbstractCoroutine的start方法 这里调用了start变量的invoke方法,因为CoroutineStart重写了invoke方法,所以能直接这么调,相当于是一个闭包的调用方式,这里调用了三参的invoke: CoroutineStart的invoke方法

  • 第一个参数:挂起函数闭包,也就是协程要执行的代码块
  • 第二个参数:start方法传进来的StandaloneCoroutine
  • 第三个参数:start方法传进来的this,也是StandaloneCoroutine

CoroutineStart是DEFAULT类型,所以会调用挂起函数的startCoroutineCancellable方法: 挂起函数的startCoroutineCancellable方法 在上面分析createCoroutine时候也是通过createCoroutineUnintercepted(receiver, completion).intercepted(),创建了delegate,最终是一个Continuation,也是一个suspendLambda。我们直接看ContinuationImpl的intercepted方法: ContinuationImpl的intercepted方法 intercepted方法里面取context变量中的key为ContinuationInterceptor的context,而context最终是completion的context,completion是前面start方法传进来的StandaloneCoroutine,它是继承自AbstractCoroutine: AbstractCoroutine parentContext是launch方法传进来的context+CoroutineScope自己的context拼接的一个context
this:AbstractCoroutine实现了job接口,job也是一个CoroutineContext
在上面例子中lauch方法是通过GlobalScope启动的,它的context是一个EmptyCoroutineContext,所以传给AbstractCoroutine的parentContext其实就是launch方法传进去的context,也就是Dispatchers.io、Dispatchers.main等。
回到ContinuationImpl的intercepted方法,取context的ContinuationInterceptor,然后调用interceptContinuation方法,看下Dispatchers.io,它最终继承自CoroutineDispatcher:
CoroutineDispatcher的interceptContinuation方法 结论: 如果lauch传的是Dispatchers.io,则lauch先创建DispatchedContinuation,然后调用resumeCancellableWith方法: DispatchedContinuation的resumeCancellableWith方法 如果dispatcher.isDispatchNeeded(默认是true),才会进入到dispatch逻辑,看下dispatchers.io: DefaultIoScheduler的dispatch方法 调用了default.dispatch方法,diefault是由UnlimitedIoScheduler.limitedParallelism创建的LimitedDispatcher,最终会执行到UnlimitedIoScheduler.dispatch方法: UnlimitedIoScheduler的dispatch方法 DefaultScheduler的dispatchWithContext方法如下: DefaultScheduler的dispatchWithContext coroutineScheduler创建如下: coroutineScheduler的创建 最终会执行到CoroutineScheduler的dispatch方法,里面的代码就不细看了,是线程池部分执行block,关于这部分后面可以深究下,而block是在DispatchedContinuation中resumeCancellableWith方法里面把this给到了dispatcher的dispatch方法,说明DispatchedContinuation实现了Runnable接口,直接看它的run方法,该方法定义在它的父类DispatchedTask中: DispatchedTask的run方法 此处执行了delegate的resume方法,resume方法执行了resumeWith,注意此处的delegate是DispatchedContinuation中传进来的continuation,它是createCoroutineUnintercepted(receiver, completion).intercepted()创建的delegate,是一个continuation对象,也是suspendlambda,在上面创建协程分析过baseContinuationImpl的resumeWith方法,里面会执行协程的invokeSuspend方法,也就是我们的协程执行代码。执行完了后会执行complete的resumeWith,而通过lauch创建的协程,此处的complete是launch方法构造的StandaloneCoroutine对象,它的resumeWith方法定义在AbstractCoroutine中: AbstractCoroutine中的resumeWith方法 AbstractCoroutine的afterCompletion: AbstractCoroutine的afterCompletion方法 此处没有逻辑执行。
我们注意到在讲解协程启动的时候,创建delegate的continuation时候调用createCoroutineUnintercepted只传了complete,没有传receiver。而在launch启动协程时候,将StandaloneCoroutine作为receiver传给了createCoroutineUnintercepted方法: 挂起函数的createCoroutineUnintercepted方法 默认的挂起函数的create方法是抛异常的,需要SuspendLambda的子类自己去实现: SuspendLambda默认的create方法 编译后的SuspendLambda的子类create实现: 编译后SuspendLamda的子类的create方法 此处将receiver传到create中没有用到,所以其实跟单参没有什么区别啊。

总结

结论:协程在启动过程中会取CoroutineContext中的ContinuationInterceptor,然后执行interceptContinuation。而此时如果是一个Dispatchers.IO,它又是继承自CoroutineDispatcher,所以会执行到了CoroutineDispatcher的interceptContinuation。此时得到的是一个DispatchedContinuation对象,并把当前的CoroutineDispatcher和协程启动过程中创建的StandaloneCoroutine传给了。接着会执行DispatchedContinuation的resumeCancellableWith方法,在该方法里面会判断CoroutineDispatcher.isDispatchNeeded,如果是的话,会调用CoroutineDispatcher的dispatch方法,最终会通过线程池会执行到DispatchedContinuation的run方法,因为它是一个Runnable对象。在run方法里面,会执行continuation的resume方法,而此处的continuation是编译器给我们创建的SuspendLambda的子类,最终会执行它的invokeSuspend方法。执行完后会执行协程启动过程中的StandaloneCoroutine的resumeWith方法。
时序图: 协程线程调度时序图

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy