本文是适合协程初学者的入门介绍,但乍看题目可能会觉得有点奇怪,怎么除了基本概念,还在入门文章中提及了深奥的原理?作为小白,应该要先接触基础概念,等熟悉后再进行原理分析才对。
本文的目标也是如此,让初学者能快速理解概念。以我自身的学习经验,单纯理解协程的挂起,恢复等概念还不如直接深入分析代码实现理解的快。当然,为了减少复杂度,本文只讲几个最最基本的概念,以及通过拆解一个最简单的demo来理解协程的自主挂起,恢复概念。
通过阅读本文,你能收获:
- 只能知道suspend关键字,协程最简单的创建流程以及启动和挂起协程
- 能完全理解协程基础原理(不夸张,因为原理真的很简单)
1. 协程是什么
协程(Coroutine)是计算机程序的一类组件,推广了协作式多任务的子程序,允许执行被挂起与被恢复,与多线程一样是一种异步编程的方式。 但与多线程依赖系统调度不同,协程由程序自主决定挂起与执行(个人觉得理解自主决定挂起与恢复是理解koltin协程的关键,后面会通过demo进行源码分析)
2. 协程怎么编程
如果你是初学者,看了协程概念后大概率也是一头雾水,没关系,我们先从koltin 协程的基础语法学起,对协程编程有个概念
A. suspend 关键字
suspend用于暂停执行当前协程,并保存所有局部变量。如需调用suspend函数,只能从其他suspend函数进行调用,或通过使用协程构建器(例如 launch)来启动新的协程。
Emmm。。看了等于没看。。
Read The Fucking Source Code --Linus Torvalds
下面是suspend使用的最简单的例子:
suspend fun test1(){
println("test1")
}
反编译后可以看到其实就是一个普通函数,它接受一个Continuation类型的参数,所以suspend关键字可以理解是一种简化写法的语法糖
@Nullable
public static final Object test1(@NotNull Continuation $completion) {
String var1 = "test1";
System.out.println(var1);
return Unit.INSTANCE;
}
Redundant suspend modeifier
有时候在使用suspend关键字时会提示以下错误,从反编译后的代码上不难看出,其实就是Continuation参数并没被使用,所以可以去掉suspend关键字
B. 创建协程
下面是kotlin创建一个协程并运行的最简demo。首先,赋值运算符左边定义了一个continuation变量,它是Continuation类型,右边语法比较复杂,我们分成三块逐步分析:
- 第一部分是一个由suspend 修饰的lambda表达式
- 第二部分是这个lambda表达式调用了createCoroutine函数。这里理解的关键是要理解在kotlin中,所有类型都是对象,所以lambda也是一个对象,lambda.createCoroutine 其实就是调用了一个对象的成员方法。(后面会分析此函数)
- 第三部分是调用createCoroutine 时传入的参数,它是一个继承于Continuation 的object对象。
把三部分连接起来,阐述的语义是:当协程启动时,会先调用第一部分的lambda执行体,执行完成后调用第三部分的obejct对象的resumeWith方法(还记得协程是异步的实现方式吗,对比多线程编程,应该可以很容易理解第一部分就是需要异步执行的内容,object对象是执行完成后的callback)
demo的最后是 continuation.resume(Unit),表示启动协程
fun main(){
// 1. 创建cotinuation对象
val continuation: Continuation<Unit> = suspend {
println("hello continuation")
}.createCoroutine(object : Continuation<Unit>{
override val context: CoroutineContext
get() = EmptyCoroutineContext
override fun resumeWith(result: Result<Unit>) {
println("end conginuation")
}
})
// 2. 运行
continuation.resume(Unit);
}
C. 通过调用 suspendCoroutine主动挂起协程
suspendCoroutine会自动捕获当前的执行环境(如临时变量, 参数等), 然后存放到一个Continuation中, 并且作为参数传给它的lambda.
先不管上面的介绍,反正我们知道通过调用suspendCoroutine,能中断协程的执行。来一段demo深度理解下:
lateinit var continuation:Continuation<Unit>
suspend fun yield() {
suspendCoroutine<Unit> {
continuation = it;
}
}
fun main(){
// 1. 创建cotinuation对象
val continuation: Continuation<Unit> = suspend {
println("hello continuation,part 1")
yield();
println("hello continuation,part 2")
}.createCoroutine(object : Continuation<Unit>{
override val context: CoroutineContext
get() = EmptyCoroutineContext
override fun resumeWith(result: Result<Unit>) {
println("end conginuation")
}
})
println("开始运行协程")
// 2. 运行
continuation.resume(Unit);
println("surprised")
}
上面程序运行结果如下。可能现在你会对输出结果感到奇怪,但正如suspendCoroutine 函数的说明,它能中断协程的执行。所以当调用continuation.resume函数执行协程时,会执行lambda里面的内容,首先打印 “hello continuation,part 1” ,然后调用yield()函数,因为它里面调用了suspendCoroutine,中止了协程的执行,即lambda的执行,所以回到continuation.resume的下一行开始继续执行,即打印"surprised"。
开始运行协程
hello continuation,part 1
surprised
是不是跟我一样惊讶于这种运行结果?好奇它底层是如何实现的? 是依靠系统调度?别急,答案马上揭晓
3. 原理分析
A. createCoroutine 函数分析
下面是createCoroutine函数的实现,读懂需要有一定的koltin语法基础。
代码很清晰地告诉我们createCoroutine是一个函数对象的扩展函数,这个函数对象是 suspend ()-> T 类型,调用createCoroutine后会返回一个SafeContinuation类型。
public fun <T> (suspend () -> T).createCoroutine(
completion: Continuation<T>
): Continuation<Unit> =
SafeContinuation(createCoroutineUnintercepted(completion).intercepted(), COROUTINE_SUSPENDED)
B. createCoroutineUnintercepted 函数分析
构造SafeContinuation对象前先调用了createCoroutineUnintercepted函数,在看代码之前,我们先对completion参数有一个明确概念,它是一个continuation对象,对于 创建协程 部分的demo来说,它就是第三部分的object对象,如果忘记了,可以再重新返回看一遍。
有了上述前提后,我们继续看代码具体阐述了什么内容。 为了简单分析,我们分析else分支的实现,这里调用了createCoroutineFromSuspendFunction函数,并传入两个参数,第一个就是completion,比较清晰。第二个是一个lambda表达式:{(this as Function1<Continuation, Any?>).invoke(it)},理解这一句的关键是理解this指针,因为createCoroutineUnintercepted也是一个对象的扩展函数,所以this肯定是指代了此对象的实例,对于 创建协程 部分的demo来说,它就是第一部分创建的lambda表达式,invoke函数就是执行这个lambda表达式。
public actual fun <T> (suspend () -> T).createCoroutineUnintercepted(
completion: Continuation<T>
): Continuation<Unit> {
// probeCoroutineCreated 直接返回 completion
val probeCompletion = probeCoroutineCreated(completion)
return if (this is BaseContinuationImpl)
create(probeCompletion)
else
createCoroutineFromSuspendFunction(probeCompletion) {
// 因为createCoroutineFromSuspendFunction是扩展函数,this指
// suspend { println("hello continuation") }
(this as Function1<Continuation<T>, Any?>).invoke(it)
}
}
接下来看createCoroutineFromSuspendFunction函数,还是看else分支,它返回了一个ContinuationImpl object,并且继承了invokeSuspend方法,最重要的是,这个方法里面调用了block(this) (block是(this as Function1<Continuation, Any?>).invoke(it))
看到这里,是不是抓住了什么? 没错,只要我们通过调用ContinuationImpl.invokeSuspend()函数,就能执行demo第一部分定义的lambda表达式,即执行 println(“hello continuation”) 语句
那么进一步,运行协程是通过调用continuation.resume(Unit)执行,那它肯定是调用到了ContinuationImpl.invokeSuspend()。事实上也确实是这样,后面也会分析
@SinceKotlin("1.3")
private inline fun <T> createCoroutineFromSuspendFunction(
completion: Continuation<T>,
crossinline block: (Continuation<T>) -> Any?
): Continuation<Unit> {
val context = completion.context
// label == 0 when coroutine is not started yet (initially) or label == 1 when it was
return if (context === EmptyCoroutineContext)
// ......
else
object : ContinuationImpl(completion as Continuation<Any?>, context) {
private var label = 0
override fun invokeSuspend(result: Result<Any?>): Any? =
when (label) {
0 -> {
label = 1
result.getOrThrow() // Rethrow exception if trying to start with exception (will be caught by BaseContinuationImpl.resumeWith
block(this) // run the block, may return or suspend
}
1 -> {
label = 2
result.getOrThrow() // this is the result if the block had suspended
}
else -> error("This coroutine had already completed")
}
}
}
C. ContinuationImpl 类分析
下面是ContinuationImpl的具体实现,没啥重要的内容,最重要的invokeSuspend我们已经分析过了。
// 继承链: ContinuationImpl -> BaseContinuationImpl -> Continuation<Any?>
internal abstract class ContinuationImpl(
completion: Continuation<Any?>?,
private val _context: CoroutineContext?
) : BaseContinuationImpl(completion) {
constructor(completion: Continuation<Any?>?) : this(completion, completion?.context)
public override val context: CoroutineContext
get() = _context!!
@Transient
private var intercepted: Continuation<Any?>? = null
public fun intercepted(): Continuation<Any?> =
intercepted
?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
.also { intercepted = it }
protected override fun releaseIntercepted() {
val intercepted = intercepted
if (intercepted != null && intercepted !== this) {
context[ContinuationInterceptor]!!.releaseInterceptedContinuation(intercepted)
}
this.intercepted = CompletedContinuation // just in case
}
}
D. BaseContinuationImpl 类分析
下面是BaseContinuationImpl的源码,里面最关键的是resumeWith 函数的实现逻辑,其他细节我们可以忽略,只需要关注主要流程:
- 在try块里调用了invokeSuspend(xx)函数
- 函数的最后调用了completion.resumeWith(outcome)
现在应该更能理解 创建协程 的三个部分了吧?通过invokeSuspend(xx)函数调用第一部分执行,第一部分执行完成后会调用第三部分,即completion的resumeWith() 函数,所以这就是为什么说completion是callback的原因。
@SinceKotlin("1.3")
internal abstract class BaseContinuationImpl(
public final override fun resumeWith(result: Result<Any?>) {
// This loop unrolls recursion in current.resumeWith(param) to make saner and shorter stack traces on resume
var current = this
var param = result
while (true) {
// Invoke "resume" debug probe on every resumed continuation, so that a debugging library infrastructure
// can precisely track what part of suspended callstack was already resumed
probeCoroutineResumed(current)
with(current) {
val completion = completion!! // fail fast when trying to resume continuation without completion
val outcome: Result<Any?> =
try {
val outcome = invokeSuspend(param)
if (outcome === COROUTINE_SUSPENDED) return
Result.success(outcome)
} catch (exception: Throwable) {
Result.failure(exception)
}
releaseIntercepted() // this state machine instance is terminating
if (completion is BaseContinuationImpl) {
// unrolling recursion via loop
current = completion
param = outcome
} else {
// top-level completion reached -- invoke and return
completion.resumeWith(outcome)
return
}
}
}
}
}
E. SafeContinuation 类分析
调用createCoroutine 函数最终返回SafeContinuation对象,此对象的构造语句如下: SafeContinuation(createCoroutineUnintercepted(completion).intercepted(), COROUTINE_SUSPENDED)
上面已经分析了createCoroutineUnintercepted(completion)返回了一个ContinuationImpl对象,后面的.intercepted()是拦截器的实现,我们先不关心,可以直接理解成SafeContinuation(ContinuationImpl,COROUTINE_SUSPENDED)。safeContinuation对象最重要的一环是重新实现了resumeWith函数,在这个函数里调用了delegate.resumeWith(result),即ContinuationImpl.resumeWith(result),ContinuationImpl又继承于 BaseContinuationImpl,固调用的是BaseContinuationImpl.resumeWith() 函数,再往后的的流程在BaseContinuationImpl章节已详细描述。
internal actual class SafeContinuation<in T>
internal actual constructor(
private val delegate: Continuation<T>,
initialResult: Any?
) : Continuation<T>, CoroutineStackFrame {
public actual override fun resumeWith(result: Result<T>) {
while (true) { // lock-free loop
val cur = this.result // atomic read
when {
cur === UNDECIDED -> if (RESULT.compareAndSet(this, UNDECIDED, result.value)) return
cur === COROUTINE_SUSPENDED -> if (RESULT.compareAndSet(this, COROUTINE_SUSPENDED, RESUMED)) {
delegate.resumeWith(result)
return
}
else -> throw IllegalStateException("Already resumed")
}
}
}
}
F. continuation.resume(Unit) 方法分析
通过continuation的resume函数可以执行一个协程,看一下它的定义
public inline fun <T> Continuation<T>.resume(value: T): Unit
= resumeWith(Result.success(value))
其实很清晰了,调用resume() 后会调用 resumeWith函数,最终会调用invokeSuspend() 函数,从而执行 创建协程 demo中的第一部分,执行println(“hello continuation”);
调用链如下:
- continuation.resume(Unit);
- SafeContinuation.resumeWith()
- ContinuationImpl::BaseContinuationImpl.resumeWith()
- ContinuationImpl.invokeSuspend --> 执行协程体
- completion.resumeWith(outcome) --> 调用返回函数
4. 以demo分析协程原理
上面章节讲述了调用continuation.resume(Unit)是如何调用到lambda执行体的过程,但并未描述挂起函数suspendCoroutine的实现,本节通过分析一个最简单的demo,能让你很好地理解协程自主挂起,恢复的原理,以及suspendCoroutine函数具体做了什么。(如果上面章节没看懂的话,没有关系,但下面的章节需要详细阅读)
A. demo初探
demo代码:
lateinit var continuation:Continuation<Unit>
suspend fun yield() {
suspendCoroutine<Unit> {
continuation = it;
}
}
fun main(){
var temp:Int = -1;
continuation = suspend {
for (i in 0..5) {
println("挂起") // ②
temp = i;
yield()
println("恢复执行") // ④
}
}.createCoroutine(object: Continuation<Unit>{
override val context: CoroutineContext
get() = EmptyCoroutineContext
override fun resumeWith(result: Result<Unit>) {
println("End generator")
}
});
for(i in 0..5) {
continuation.resume(Unit); // ①
println("从协程体退出," + temp) // ③
println("\n")
}
continuation.resume(Unit);
}
上面的代码仿造了python genertor生成器的实现,点击运行后,输出结果如下:
挂起
从协程体退出,0
恢复执行
挂起
从协程体退出,1
恢复执行
挂起
从协程体退出,2
恢复执行
挂起
从协程体退出,3
恢复执行
挂起
从协程体退出,4
恢复执行
挂起
从协程体退出,5
恢复执行
End generator
简单分析下执行过程,先对过程有个了解,方便往后继续看原理。
- ①处执行了continuation.resume() 函数后,执行lambda表达式②(为了方便,下面用协程体指这一部分)。 首先进入for循环,输出"挂起",接下来调用yield() 函数
- 调用yield() 函数后从协程体退出,返回③处继续执行,打印输出
- 之后又调用continuation.resume()从①处继续执行协程体,此时会从上次中断的位置往后执行,即④处输出"恢复执行"
- 重复上述流程
B. 分析demo
将kotlin 代码反编译如下(省略了一些细节代码,我们只关心主要流程)
// invokeSuspend是重点,看不懂的话多看几遍
public final Object invokeSuspend(@NotNull Object $result) {
Object var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
int i;
int var3;
String var4;
switch (this.label) {
case 0:
ResultKt.throwOnFailure($result);
i = 0;
var3 = 5;
break;
case 1:
var3 = this.I$1;
i = this.I$0;
ResultKt.throwOnFailure($result);
var4 = "恢复执行";
System.out.println(var4);
++i;
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
while(i <= var3) {
var4 = "挂起";
System.out.println(var4);
temp.element = i;
this.I$0 = i;
this.I$1 = var3;
this.label = 1;
if (GeneratorKt.yield(this) == var5) {
return var5;
}
var4 = "恢复执行";
System.out.println(var4);
++i;
}
return Unit.INSTANCE;
}
@Nullable
public static final Object yield(@NotNull Continuation $completion) {
SafeContinuation var2 = new SafeContinuation(IntrinsicsKt.intercepted($completion));
Continuation it = (Continuation)var2;
int var4 = false;
continuation = it;
Object var10000 = var2.getOrThrow();
if (var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED()) {
DebugProbesKt.probeCoroutineSuspended($completion);
}
return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : Unit.INSTANCE;
}
其实看到代码时已经豁然开朗,醍醐灌顶… 这不就一个状态机吗
来慢慢拆解下,我们知道调用continuation.resume()最终会调用到一个invokeSuspend函数,没错,就是上述代码里的invokeSuspend函数,它能被多次调用到。下面根据demo来理解下代码:
- 在demo的第一次for循环中调用continuation.resume()①执行了协程体,即调用了invokeSuspend函数,此时第一次调用,label为0,固走了case 0 的分支,初始化了for循环的变量。 之后进入while语句块,先输出"恢复执行",将label设为1(这里是第二次运行的关键),然后调用了GeneratorKt.yield(this)函数,即调用了suspendCoroutine函数,这个函数没有具体的实现,好像是依赖具体平台实现,我没找到实现代码,有兴趣的可以关注下。 反正我们知道suspendCoroutine函数会返回COROUTINE_SUSPENDED,所以反编译后的yield函数会返回COROUTINE_SUSPENDED。回到while语句块,它会判断yield返回值是否是COROUTINE_SUSPENDED,如果是则直接return,如果不是继续往下执行
- 因为yield函数返回COROUTINE_SUSPENDED,所以走了return的逻辑,从continuation.resume()的调用中返回,并从它的下一句代码③开始执行,所以接下来执行println(“从协程体退出,” + temp) 。
- 第二次for循环又调用continuation.resume()①,进入invokeSuspend函数,此时lable为1,所以走了case 1 的分支,输出了"恢复执行",之后根据this中的成员变量初始化临时变量,继续往下执行,进入while语句块,这里跟1流程一样
- 重复上述过程
总结
通过上面的分析,kotlin协程在我们面前已经毫无秘密。它并不复杂,只是一个上层框架,里面封装了一个状态机,后续会通过state多次调用同个函数,即invokeSuspend函数,当然,在每次调用后都会保存一些所需变量,在下次调用时还原现场。
invokeSuspend函数由编译器根据suspend ()->T 实例的实际情况自动生成多个case分支,suspendCoroutine函数为我们提供了在invokeSuspend函数中直接return,中断协程体继续执行的手段,当然,在中断前会通过成员变量保存上次运行的现场,在下次调用continuation.resume() 时能在中断的位置继续执行.
概念回顾
理解了原理,我们再回头看下各种概念的定义:
- 协程:协程的概念最核心的点就是函数或者一段程序能够被挂起,稍后再在挂起的位置恢复。挂起和恢复是开发者的程序逻辑自己控制的
- 协程挂起:在协程体,即协程代码的执行中插入一条return语句,当然,在return前通过对象的成员变量,如this.I$0,this.I$2… 对变量进行保存,方便下次执行时恢复现场
- 协程恢复:重新调用invokeSuspend函数,先恢复现场,并根据上次运行的结果确定case 分支,继续往下执行
- 自主挂起与恢复:可以看到程序一直在单线程运行,挂起与恢复的概念并不像多线程一样是系统调度的概念,完全是通过状态机,即switch(label)实现,第一次调用时label状态为0,之后lable状态为1,所以是通过编程实现,固说程序具有完全的自主控制权
- 协程是否像多线程一样有自己的栈空间和local 变量?
没有,协程就是普通函数和对象。只不过这个函数具备被挂起和恢复执行的能力。如何提供这种能力?在kotlin中依靠状态机和switch语句块实现。 - 协程挂起后恢复执行时做了什么? 有无调用栈恢复,即如何保存上次运行现场?
从分析demo的章节中,我们可以看到创建协程实际上是创建了一个对象,这个对象有一个suspendInvoke方法,并且有swicth(label)语句块,通过状态label区分多次调用suspendInvoke的逻辑,在挂起协程时,会通过this.I$0,this.I$2… 对变量进行保存,下次调用时恢复。所以保存现场是通过类成员变量的方式保存