前言
协程是一种并发设计模式(接下来我所说的协程均指kotlin协程),利用协程我们可以简化异步执行的代码,但在使用协程的时候,相信大部分人都是摸着石头过河,跟着官方模板写,至于协程更高级的用法,那是能免则免,咱也把控不住,毕竟出了问题不好排查,所以深入理解协程原理能让我们对协程的使用更有掌控力,而理解协程的挂起和恢复是揭开协程原理面纱的第一步。
协程的挂起和恢复是指什么
关于挂起这个词我们可能更多想到的是线程的挂起,事实上两者想表述的意思是类似的,线程挂起是指当前线程让出cpu资源,暂时不可执行,协程也是一样,协程的挂起指的是协程体的挂起,把资源交还给当前线程,在google官方文档介绍协程时特意提到了这点,
你可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
在单一线程上可以执行多个协程,那么协程到底是什么,在真正开始了解协程挂起机制之前,我们必须要知道是什么东西在挂起,这里的协程我们可以简单理解为协程体,什么是协程体,我们暂且理解协程体是一段代码块,那么接下来我们要讨论的便是代码块的挂起和恢复。
如何实现协程体的挂起和恢复
在上面我们提到协程的挂起指的是协程体的挂起(代码块),那如何让一段代码块挂起呢,如果我们能实现代码块的挂起和恢复,那么我们只需要再将协程代码转换成特定代码块就可以实现协程的挂起和恢复。先来一段简单的代码块
println("1")
println("2")
复制代码
够简单吧,那么如何挂起它呢,所谓的挂起就是在同步执行的时候绕过这个代码块,待会儿回来再执行它,这是不是跟我们平时使用的回调很相似,我们试试用回调来实现上面这段代码的挂起,来个最经典的吧
view.setOnClickListener {
println("1")
println("2")
}
复制代码
这个代码大家再熟悉不过了,我们很简单的实现了代码块的挂起,并且在view点击的时候恢复代码的执行。而协程体的挂起和恢复也是同样的道理,什么是协程体,现在我们就来揭秘,协程体就是Continuation
public interface Continuation<in T> {
/**
* The context of the coroutine that corresponds to this continuation.
*/
public val context: CoroutineContext
/**
* Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
* return value of the last suspension point.
*/
public fun resumeWith(result: Result<T>)
}
复制代码
这时候有人就要说了,兄弟你是在忽悠我吧,这玩意儿跟我们写的回调有鸡毛关系,大家稍安勿躁,我们先来看看刚刚的view点击事件是怎么实现的,我们传入了一个OnClickListener,这个OnClickListener携带了一段代码块,现在我们来看看OnClickListener接口的定义
public interface OnClickListener {
/**
*
* @param v The view that was clicked.
*/
void onClick(View v);
}
复制代码
好像有那么点感觉了,这个onClick方法不就是对标Continuation.resumeWith()嘛,我们是在view点击的时候触发onClick来回调,也就是恢复代码块的执行,同理协程体也是通过触发resumeWith来恢复代码的执行,这就是协程体的恢复,至于挂起,也跟setOnClickListener一样,我们只需要构造一个Continuation,暂时不执行它,到这里我们已经大致了解协程体是如何挂起和恢复了,但是这个Continuation跟我们平常写的协程看起来似乎并没有任何关联性。
协程的创建
单纯讲协程创建很枯燥且难以吸收,我们需要结合协程创建目的一起来看,在上面我们分析了协程体的挂起和恢复,也就是说我们只要将协程代码转换成协程体再给予一些协程体调度的能力就能实现挂起和恢复了,我们先来写一段协程代码,还是用上面的代码块吧
suspend fun testCode(){
println("1")
println("2")
}
复制代码
这是一个suspend函数,也就是挂起函数,它现在跟协程体没有半毛钱关系,现在我们想要挂起它,直接调用肯定是不行的,ide会提示你
//挂起函数只能在挂起函数或者协程内被调用
Suspend function 'testCode' should be called only from a coroutine or another suspend function
复制代码
如何挂起它呢,按照上面的分析,我们得先把它转换成协程体(Continuation),我们拿到了Continuation,就可以通过resumeWith方法随时恢复它,平时我们都是通过协程库封装好的构造器来创建协程,比如:
xxx.launch{}
复制代码
其内部实现屏蔽了协程的创建和启动等细节,为了更清晰的分析协程启动,我们使用协程标准库提供的创建协程方法:
public fun <T> (suspend () -> T).createCoroutine( completion: Continuation<T>): Continuation<Unit> =
SafeContinuation(createCoroutineUnintercepted(completion).intercepted(), COROUTINE_SUSPENDED)
复制代码
如我们所愿,该方法返回了一个Continuation,但还需要一个入参,类型同样为Continuation,这个入参是干嘛的呢,现在暂时不知道,我们待会再说,我们先跟着上面回调的思路来模拟协程体的挂起:
//创建协程体,拿到了这个协程体我们就可以挂起testCode函数,且在任意时刻恢复它
val continuation = ::testCode.createCoroutine(object : Continuation<Unit> {
override val context: CoroutineContext
get() = EmptyCoroutineContext
override fun resumeWith(result: Result<Unit>) {
BaseLog.d("我被恢复了")
}
})
println(3)
//我们可以在任何时刻恢复testCode代码块的执行
continuation.resumeWith(Result.success(Unit))
复制代码
可以看到当我们将testCode函数转换成Continuation后,可以选择立马执行resumeWith,也可以先挂起,稍后再执行resumeWith,我们可以轻松拿捏testCode函数的挂起和恢复。
协程挂起和恢复的原理
在上面的代码里我们创建了协程体Continuation,并通过resumeWith来恢复协程,那么它是怎么恢复testCode代码块的执行呢,我们以上面代码为例来分析整个调用的过程,首先我们调用了标准库的方法来构建协程体,可以看到它返回的是一个SafeContinuation, 先看看SafeContinuation.resumeWith实现
SafeContinuation
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")
}
}
}
复制代码
这是个死循环,只有result的值通过cas转变成RESUMED状态时才会跳出循环,result的初始值就是COROUTINE_SUSPENDED,单线程情况下cas会立马返回true,进而调用delegate.resumeWith(result),结束死循环,这个delegate又是啥呢,我们通过反射打印下它的类名:
kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt$createCoroutineUnintercepted$$inlined$createCoroutineFromSuspendFunction$IntrinsicsKt__IntrinsicsJvmKt$1
复制代码
好家伙,名字这么长,显然这个类是编译期生成,也就是我们所谓的协程体(至于为什么,当然是猜的,后面我们慢慢验证),我们反编译之后去看看该类的实现,由于名字过长且代码太过抽象,我对其进行了ps,现在假设这个类就叫Delegate,那么Delegate的实现为:
public final class Delegate extends ContinuationImpl {
final /* synthetic */ Continuation $completion;
final /* synthetic */ CoroutineContext $context;
final /* synthetic */ Object $receiver$inlined;
final /* synthetic */ Function2 function2;
private int label;
public Delegate(Continuation $captured_local_variable$1, CoroutineContext $captured_local_variable$2, Continuation $super_call_param$3, CoroutineContext $super_call_param$4, Function2 function2, Object obj) {
super($super_call_param$3, $super_call_param$4);
this.$completion = $captured_local_variable$1;
this.$context = $captured_local_variable$2;
this.function2 = function2;
this.$receiver$inlined = obj;
}
@Override // kotlin.coroutines.jvm.internal.BaseContinuationImpl
public Object invokeSuspend(Object result) {
int i = this.label;
if (i == 0) {
this.label = 1;
ResultKt.throwOnFailure(result);
Delegate delegate = this;
Function2 function2 = this.function2;
if (function2 != null) {
return function2.invoke(this.$receiver$inlined, delegate);
}
throw new NullPointerException("null cannot be cast to non-null type (R, kotlin.coroutines.Continuation<T>) -> kotlin.Any?");
} else if (i == 1) {
this.label = 2;
ResultKt.throwOnFailure(result);
return result;
} else {
throw new IllegalStateException("This coroutine had already completed".toString());
}
}
}
复制代码
即使ps过后该类依旧难以阅读,但是我们必须迎男而上,可以看到Delegate继承了ContinuationImpl,进而继承了BaseContinuationImpl,而BaseContinuationImpl实现了Continuation接口(果然是协程体),Delegate.resumeWith其实是调用了BaseContinuationImpl.resumeWith
BaseContinuationImpl
@Override // kotlin.coroutines.Continuation
public final void resumeWith(Object result) {
Object outcome;
BaseContinuationImpl baseContinuationImpl = this;
Object param = result;
while (true) {
DebugProbesKt.probeCoroutineResumed(baseContinuationImpl);
//1、前面创建协程传入的参数
Continuation completion2 = baseContinuationImpl.completion;
Intrinsics.checkNotNull(completion2);
try {
//2、执行状态机
Object outcome2 = baseContinuationImpl.invokeSuspend(param);
//3、如果需要挂起,直接return,如果没有挂起继续往下走
if (outcome2 != IntrinsicsKt.getCOROUTINE_SUSPENDED()) {
Result.Companion companion = Result.Companion;
outcome = Result.m4constructorimpl(outcome2);
baseContinuationImpl.releaseIntercepted();
if (completion2 instanceof BaseContinuationImpl) {
baseContinuationImpl = (BaseContinuationImpl) completion2;
param = outcome;
} else {
//4、如果completion2不是BaseContinuationImpl类型
completion2.resumeWith(outcome);
return;
}
} else {
return;
}
} catch (Throwable exception) {
Result.Companion companion2 = Result.Companion;
outcome = Result.m4constructorimpl(ResultKt.createFailure(exception));
}
}
}
复制代码
这段代码依旧难以下咽,不过大家可以跟着我的注释快速食用,
- 首先是一个死循环,在走到注释2的时候会调用我们常说的状态机,也就是前面贴的Delegate的invokeSuspend方法,大家自己回去看,当label为0时执行了function2.invoke,这里的function2就是我们前面的testCode代码块。
- 再看注释3,根据invokeSuspend的结果来决定是否挂起,如果需要挂起,直接return结束死循环,在我们的代码案例中此时是不需要挂起的,所以会进入if判断体,这里的completion2是我们在创建协程体时传入的参数,可以看到当completion2类型不是BaseContinuationImpl时候才会结束死循环,这个时候也可以回答这个入参的作用了,它的作用就是用来终止死循环,有人可能会说如果我的入参是一个BaseContinuationImpl怎么办,不好意思,要构建BaseContinuationImpl还得再传一个Continuation,也就是协程体可以无限套娃,但是最终需要一个非BaseContinuationImpl类型的出口来终止死循环。
真实的挂起
在上面的案例中我们并没有看到真正的挂起,因为上面的案例没有线程切换的操作,理解协程的线程调度需要更多的储备知识,如果加上线程切换的代码和解读,整个篇幅会变得一发不可收拾,并不利于我们去理解协程挂起的本质。对于真正需要挂起协程的场景,我们可以就上面案例简单分析下,当invokeSuspend返回COROUTINE_SUSPENDED时,BaseContinuationImpl.resumeWith会直接结束死循环,让出线程资源,那么如何恢复呢,在invokeSuspend方法里我们看到真正执行代码块的内容是function2,而我们在调用function2时传入了当前的Delegate,真正挂起时,function2执行完后会通过传入的delegate再次执行resumeWith方法,进而继续调用invokeSuspend,此时invokeSuspend里面label值已经完成了累加,会接着执行当前协程体剩余的代码块,并根据实际情况再次决定是否需要挂起,以上步骤周而复始,直到当前协程体不需要挂起且执行完毕时跳出死循环。
简单总结下
1、协程通过Continuation来实现挂起和恢复
2、编译期生成的协程体实现了BaseContinuationImpl并重写了invokeSuspend
3、invokeSuspend返回的值决定当前协程体是否需要挂起