Kotlin Coroutines 完全解析(二),深入理解协程的挂起、恢复与调度

文章转载自:

https://www.jianshu.com/p/2979732fb6fb

本文基于 Kotlin v1.3.0-rc-146,Kotlin-Coroutines v1.0.0-RC1

前面一篇文章协程简介,简单介绍了协程的一些基本概念以及其简化异步编程的优势,但是协程与线程有什么区别,协程的挂起与恢复是如何实现的,还有协程运行在哪个线程上,依然不是很清楚。这篇文章将分析协程的实现原理,一步步揭开协程的面纱。先来看看协程中最关键的挂起函数的实现原理:

1. 挂起函数的工作原理

协程的内部实现使用了 Kotlin 编译器的一些编译技术,当挂起函数调用时,背后大致细节如下:

挂起函数或挂起 lambda 表达式调用时,都有一个隐式的参数额外传入,这个参数是类型,封装了协程恢复后的执行的代码逻辑。

用前文中的一个挂起函数为例:

实际上在 JVM 中更像下面这样:

的定义如下,类似于一个通用的回调接口:

现在再看之前函数:

然而,协程内部实现不是使用普通回调的形式,而是使用状态机来处理不同的挂起点,大致的 CPS(Continuation Passing Style) 代码为:

上面代码中每一个挂起点和初始挂起点对应的 Continuation 都会转化为一种状态,协程恢复只是跳转到下一种状态中。挂起函数将执行过程分为多个 Continuation 片段,并且利用状态机的方式保证各个片段是顺序执行的。

1.1 挂起函数可能会挂起协程

挂起函数使用 CPS style 的代码来挂起协程,保证挂起点后面的代码只能在挂起函数执行完后才能执行,所以挂起函数保证了协程内的顺序执行顺序。

在多个协程的情况下,挂起函数的作用更加明显:

上面的例子中,挂起函数挂起当前协程,直到异步协程完成执行,但是这里并没有阻塞线程,是使用状态机的控制逻辑来实现。而且挂起函数可以保证挂起点之后的代码一定在挂起点前代码执行完成后才会执行,挂起函数保证顺序执行,所以异步逻辑也可以用顺序的代码顺序来编写。

注意挂起函数不一定会挂起协程,如果相关调用的结果已经可用,库可以决定继续进行而不挂起,例如的返回值的结果已经可用时,挂起函数可以直接返回结果,不用再挂起协程。

1.2 挂起函数不会阻塞线程

挂起函数挂起协程,并不会阻塞协程所在的线程,例如协程的挂起函数会暂停协程一定时间,并不会阻塞协程所在线程,但是函数会阻塞线程。

看下面一个例子,两个协程运行在同一线程上:

运行结果为:

从上面结果可以看出,当协程 1 暂停 200 ms 时,线程并没有阻塞,而是执行协程 2 的代码,然后在 200 ms 时间到后,继续执行协程 1 的逻辑。所以挂起函数并不会阻塞线程,这样可以节省线程资源,协程挂起时,线程可以继续执行其他逻辑。

1.3 挂起函数恢复协程后运行在哪个线程

协程的所属的线程调度在前一篇文章《协程简介》中有提到过,主要是由协程的控制,可以指定协程运行在某一特定线程上、运作在线程池中或者不指定所运行的线程。所以协程调度器可以分为和,、和属于,都指定了协程所运行的线程或线程池,挂起函数恢复后协程也是运行在指定的线程或线程池上的,而属于,协程启动并运行在 Caller Thread 上,但是只是在第一个挂起点之前是这样的,挂起恢复后运行在哪个线程完全由所调用的挂起函数决定。

输出如下:

上面第三行输出,经过挂起函数后,使用的协程挂起恢复后依然在函数使用的上。

2. 协程深入解析

上面更多地是通过 demo 的方式说明挂起函数函数的一些特性,但是协程的创建、启动、恢复、线程调度、协程切换是如何实现的呢,还是不清楚,下面结合源码详细地解析协程。

2.1 协程的创建与启动

先从新建一个协程开始分析协程的创建,最常见的协程创建方式为,关键源码如下:

默认情况下会走到,最终会调用到。

重点注意该方法的注释,创建一个协程,创建了一个新的可挂起计算,通过调用启动该协程。而且返回值为,提供了恢复协程的接口,用以实现协程恢复,封装了协程的代码运行逻辑和恢复接口。

再看之前协程代码编译生成的内部类,协程的计算逻辑封装在方法中,而的继承关系为 SuspendLambda -> ContinuationImpl -> BaseContinuationImpl -> Continuation,其中 部分关键源码如下:

而这部分与之前的分析也是吻合的,启动协程流程是->->,协程的挂起通过挂起函数实现,协程的恢复通过实现。

2.2 协程的线程调度

协程的线程调度是通过拦截器实现的,前面提到了协程启动调用到了,该方法实现为:

再看的具体实现:

所以最终会使用协程的的方法包装原来的 Continuation,拦截所有的协程运行操作。

拦截了协程的启动和恢复,分别是和重写的:

继续跟踪、等方法的实现,发现其实都调用了方法,而的实现更简单,关键在于返回为。

2.3 协程的挂起和恢复

Kotlin 编译器会生成继承自的子类,协程的真正运算逻辑都在中。但是协程挂起的具体实现是如何呢?先看下面示例代码:

其中 launch 协程编译生成的 SuspendLambda 子类的方法如下:

上面代码中 launch 协程挂起的关键在于,如果此时 async 线程未执行完成,返回为,就会 return,launch 协程的方法执行完成,协程所在线程继续往下运行,此时 launch 线程处于挂起状态。所以协程挂起就是协程挂起点之前逻辑执行完成,协程的运算关键方法执行完成,线程继续执行往下执行其他逻辑。

协程挂起有三点需要注意的:

启动其他协程并不会挂起当前协程,所以和启动线程时,除非新协程运行在当前线程,则当前协程只能在新协程运行完成后继续执行,否则当前协程都会马上继续运行。

协程挂起并不会阻塞线程,因为协程挂起时相当于执行完协程的方法,线程继续执行其他之后的逻辑。

挂起函数并一定都会挂起协程,例如挂起函数如果返回值不等于,则协程继续执行挂起点之后逻辑。

下面继续分析的实现原理,它的实现中关键是调用了方法:

上面源码中的方法的逻辑就是调用恢复协程。函数里面是如何实现 async 协程完成后自动恢复之前协程的呢,源码实现有些复杂,因为很多边界情况处理就不全部展开,其中最关键的逻辑如下:

接下来我断点调试 launch 协程恢复的过程,从 async 协程的的子类的 -> ..-> -> -> -> ,最后 handler 节点里面通过调用恢复协程。

而这过程中有两个的 方法,一个是的父类的,我们再来详细分析一篇:

接下来再来看另外一类 Continuation,AbstractCoroutine 的实现:

所以其中一类 Continuation 的封装了协程的运算逻辑,用以协程的启动和恢复;而另一类 Continuation ,主要是负责维护协程的状态和管理,它的则是完成协程,恢复调用者协程。

2.4 协程的三层包装

常用的和返回的、,里面封装了协程状态,提供了取消协程接口,而它们的实例都是继承自,它是协程的第一层包装。第二层包装是编译器生成的的子类,封装了协程的真正运算逻辑,继承自,其中属性就是协程的第一层包装。第三层包装是前面分析协程的线程调度时提到的,封装了线程调度逻辑,包含了协程的第二层包装。三层包装都实现了接口,通过代理模式将协程的各层包装组合在一起,每层负责不同的功能。

下面是协程运行的流程图:

3. 小结

经过以上解析之后,再来看协程就是一段可以挂起和恢复执行的运算逻辑,而协程的挂起是通过挂起函数实现的,挂起函数用状态机的方式用挂起点将协程的运算逻辑拆分为不同的片段,每次运行协程执行的不同的逻辑片段。所以协程有两个很大的好处:一是简化异步编程,支持异步返回;而是挂起不阻塞线程,提供线程利用率。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20181124A13L2L00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券

玩转腾讯云 有奖征文活动