挂起函数既可以像普通函数一样同步返回,又可以处理异步逻辑。是如何实现的?
所谓协程的挂起其实就是程序执行流程发生异步调用时,当前调用流程的执行状态进入等待状态。请注意,挂起函数不一定真的会挂起,只是提供了挂起的条件。那什么情况下才会真正挂起呢?
让我们先看这个挂起函数:
kotlin
// #1
suspend fun outer(): Int {
var res1 = inner1()
var res2 = inner2()
return res1 + res2
}
内部有两个挂起点:inner1、inner2。执行中需要处理两次挂起并恢复执行的逻辑。如果两个挂起函数都异步返回,那么等价于这个:
kotlin
// #2
outer(completion:Continuation){
inner1((res1)=>{
inner2((res2)=>{
// return res1 + res2:返回值并非真的返回,而是将计算的结果交给外部传入的回调处理。
completion.resumeWith(res1 + res2)
})
})
}
当然,如果实际执行中 inner1、inner2 均未挂起并同步返回,那么 outer 也不会真的挂起,最终执行流程依然如 #1
所示,而不会变成 #2
的样子。Kotlin 编译器如何实现使用同步代码,同时兼顾同步和异步两种不同执行流程,编译后的代码会给出答案。
挂起函数的编译后结构是一个状态机
CPS 变换(Continuation-Passing-Style Transformation) 是Kotlin异步执行同步代码的基础:通过传递 Continuation 来控制异步调用流程。
例:
kotlin
// 源码
suspend fun doTaskA(): A = ...
// CPS变换后(伪代码)
fun doTaskA(continuation: Continuation<A>): Any? { ... }
返回值类型为 A
的挂起函数,被编译后,参数多了一个 Continuation<A>
,返回值变成 Object
。
具体来说,返回值有两种可能:
- 挂起函数内部无挂起耗时操作,函数未挂起,直接同步返回,返回值类型为
A
。 - 挂起函数内部有耗时操作,函数挂起,返回值为挂起标志
IntrinsicsKt.getCOROUTINE_SUSPENDED()
也就是说,外层挂起函数通过检查外层挂起函数的同步返回值是否为挂起标志,就能够判断内层挂起函数是否真正挂起。
因此我们可以理解为何 Kotlin 不推荐在挂起函数中直接执行耗时操作,而是使用 withContext
等方法将耗时操作切换到其他线程,并封装为挂起函数。其原因是外层函数必须拿到挂起标志,才能判断内层函数异步返回,直接进行耗时操作会导致挂起函数无法挂起。
挂起函数新增了一个 Continuation 参数,这也解释了为何只有挂起函数才能调用挂起函数。
kotlin
// 这个挂起函数实际上没有起到挂起函数应有的作用
suspend fun fakeSuspend(){
Thread.sleep(1000)
return 0
}
继续看这个挂起函数:
kotlin
suspend fun outer(): Int {
var res1 = inner1()
var res2 = inner2()
return res1 + res2
}
编译后的伪代码如下所示。
编译后,挂起函数主体成为一个状态机。其状态储存在 Continuation.label 中。通过状态转换决定下一步执行代码的位置。完整异步执行流程如下:
首次调用初始化 myContinuation.label = 0
,进入状态#0
,转变 myContinuation.label = 1
,下一个状态为#1
。调用 inner1
,根据返回值判断内层挂起函数是否挂起。若同步返回则直接进入状态#3
处理。若挂起则直接返回,等待 inner1
调用myContinuation.resume()
后,从状态#1
中获取 inner1
结果,进入同步代码块。
同步代码块中调用 inner2,如同步返回则直接返回,如异步则进入 #2
中计算rs1 + res2。至此处理过程完毕。
kotlin
public final Object outer(Continuation<?> continuation) {
// 检查是否外部进入
val myContinuation = continuation is OuterStateMachine ?
continuation : OuterStateMachine(completion = continuation)
var result: Any? = null
// 状态机主循环
when (myContinuation.label) {
0 -> { // #0 初始状态
myContinuation.label = 1
// 调用 inner1()
val suspendResult = this.inner1(myContinuation)
// 检查是否挂起
if (suspendResult == COROUTINE_SUSPENDED) {
return COROUTINE_SUSPENDED
}
// 直接获得结果,继续执行
result = suspendResult
}
1 -> { // #1 恢复状态(inner1 完成)
result = myContinuation.result // 从 Continuation 获取 inner1 的结果
}
2 -> { // #2 恢复状态(inner2 完成)
val res1 = myContinuation.res1 // 从 Continuation 中读取恢复 res1 的计算结果
result = myContinuation.result // 获取 inner2 的结果
return res1 + (result as Int) // 最终计算
}
else -> throw IllegalStateException()
}
// 同步处理状态 #3
// 处理 inner1 的结果
val res1 = result as Int
myContinuation.res1 = res1 // 保存 res1 到状态机
myContinuation.label = 2 // 更新状态
// 调用 inner2()(可能挂起)
val suspendResult = this.inner2(myContinuation)
if (suspendResult == COROUTINE_SUSPENDED) {
return COROUTINE_SUSPENDED
}
// 直接获得结果,最终计算
val res2 = suspendResult as Int
return res1 + res2
}
class OuterStateMachine(completion: Continuation<*>) : ContinuationImpl(completion) {
var res1: Int = 0 // 保存中间结果 res1
override fun invokeSuspend(result: Any?): Any? {
this.result = result
label = label or Integer.MIN_VALUE
return (this@TestCoroutine).outer(this) // 入口进入 outer
}
}
Continuation 的本质是状态保存,以及 Continuation 的树形结构
Kotlin 协程挂起时就将挂起点的信息保存到了 Continuation 对象中。Continuation 携带了协程继续执行所需要的上下文,恢复执行的时候只需要执行它的恢复调用并且把需要的参数或者异常传入即可。作为一个普通的对象,Continuation 占用内存非常小,这也是无栈协程能够流行的一个重要原因。
从 java 字节码中反编译出的:
java
$continuation = new ContinuationImpl(var1) {
// $FF: synthetic field
Object result;// 保存计算结果
int label; // 保存状态
Object L$0; // 保存协程接收者对象
int I$0; // 保存中间结果
// 状态机入口,由 ContinuationImpl.resumeWith(result)调用
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
this.result = $result;
this.label |= Integer.MIN_VALUE;
return TestCoroutine.this.outer(this);
}
};
附录
附录 1:挂起函数反编译原始代码
java
public final Object outer(@NotNull Continuation var1) {
Object $continuation;
label27: {
if (var1 instanceof <undefinedtype>) {
$continuation = (<undefinedtype>)var1;
if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) {
((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
break label27;
}
}
$continuation = new ContinuationImpl(var1) {
// $FF: synthetic field
Object result;
int label;
Object L$0;
int I$0;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
this.result = $result;
this.label |= Integer.MIN_VALUE;
return TestCoroutine.this.outer(this);
}
};
}
Object var10000;
int res1;
label22: {
Object $result = ((<undefinedtype>)$continuation).result;
Object var6 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch (((<undefinedtype>)$continuation).label) {
case 0:
ResultKt.throwOnFailure($result);
((<undefinedtype>)$continuation).L$0 = this;
((<undefinedtype>)$continuation).label = 1;
var10000 = this.inner1((Continuation)$continuation);
if (var10000 == var6) {
return var6;
}
break;
case 1:
this = (TestCoroutine)((<undefinedtype>)$continuation).L$0;
ResultKt.throwOnFailure($result);
var10000 = $result;
break;
case 2:
res1 = ((<undefinedtype>)$continuation).I$0;
ResultKt.throwOnFailure($result);
var10000 = $result;
break label22;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
res1 = ((Number)var10000).intValue();
((<undefinedtype>)$continuation).L$0 = null;
((<undefinedtype>)$continuation).I$0 = res1;
((<undefinedtype>)$continuation).label = 2;
var10000 = this.inner2((Continuation)$continuation);
if (var10000 == var6) {
return var6;
}
}
int res2 = ((Number)var10000).intValue();
return Boxing.boxInt(res1 + res2);
}
1