协程 根据 是否保存切换 调用栈 ,分为:
- 有栈协程(stackful coroutine)
- 无栈协程(stackless coroutine)
在代码上的区别是:是否可在普通函数里调用,并暂停其执行。
Kotlin协程,必须在挂起函数中调用和恢复,属于 无栈协程。
常见的语言,协程实现:
- 有栈协程:Go、Lua
- 无栈协程:Kotlin、C++ 20、Clojure、JavaScript
二、无栈协程 和 Continuation
2.1 CPS(Continuation-passing-style)
在上篇源码分析中,不难发现 执行的结果,都是通过 Continuation 来返回。
2.1.1 Continuation
Continuation
就是 一个通用的回调接口,返回 Result<T>
值 或 异常。
Continuation is a generic callback interface. —— Roman Elizarov
public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}
2.1.2 CPS
挂起函数 调用 其他挂起函数时,会将自己的
Continuation
对象 作为completion
参数 传递,
这种传递Continuation的方式,称为 连续传递风格(Continuation-passing-style),简称为 CPS。
挂起函数 编译后,会创建基于 ContinuationImpl
对象,把 调用者Continuation
传给 completion
构造参数:
internal abstract class BaseContinuationImpl(
public val completion: Continuation<Any?>?
)
2.1.3 Continuation结果返回
上篇知道 协程执行在 BaseContinuationImpl.resumeWith
方法,
同样 结果返回逻辑 也在这里,看下代码:
和 传递逻辑顺序 相反,结果按 逐步向上 返回。
分析:当获取结果后,通过 while
循环,completion
将结果向上传递,一般是协程 StandaloneCoroutine
作为最终的 completion
完成结果回调。
2.2 状态机
无栈协程,是通过 状态机 和 状态 保存恢复 来实现协程挂起恢复。
和 每个 回调 都要创建 回调对象 相比,状态机 通过
状态
记录 执行位置,当 挂起函数完成后,只需
恢复状态
接着执行后面的代码。其实就是通过
switch(label)
做判断,判断位置执行。
状态机 vs 回调,有以下几个优点:
- 复用 方法对象和状态,避免每次分配对象
- 简化 循环 和 使用 高阶函数
以下面 请求解析数据 为例,launch {}
对应的 lambda挂起函数 ,分析 Kotlin 状态机 和 状态:
GlobalScope.launch {
// 挂起点1
val data = getData()
// 挂起点2
val result = parseData(data)
println("data: $data, result: $result")
}
Kotlin编译后逻辑,以 伪代码 表示:
class $main$1 extends SuspendLambda {
// 挂起点的位置
int label;
// 状态 对象 保存 和 恢复
Object L$0;
// 更多状态: L$1 L$2 ...
Object invokeSuspend(Object result) {
Object obj;
switch (this.label) {
case 0:
this.label = 1;
obj = getData(this);
// 表示挂起,存储 状态 label = 1,
// 恢复时再次调用 invokeSuspend,恢复执行下面
if (obj == COROUTINE_SUSPENDED) {
return COROUTINE_SUSPENDED;
}
// 没有break,如果没有挂起,直接 执行下面的过程
case 1:
// 挂起恢复后
String data = (String) result;
// 如果没有挂起,直接执行则是:
// String data = (String) obj;
this.label = 2;
// 保存 状态
this.L$0 = data;
obj = parseData(data, this);
if (obj == COROUTINE_SUSPENDED) {
return COROUTINE_SUSPENDED;
}
case 2:
// 挂起恢复后
Integer num = (Integer) result;
// 如果没有挂起,直接执行则是:
// Integer num = (Integer) obj;
// 恢复状态
String data = (String) this.L$0;
System.out.println("data: " + data + ",num: " + num);
return Unit.INSTANCE;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
}
}
2.3 CPS Transform
上面说到调用挂起函数 continuation
会作为函数参数传递,但是 声明挂起函数时,
并没有 continuation
参数。而是 Kotlin 会在参数列表 自动加上 Continuation
参数,这个操作叫做 CPS Transform
。
举例,下面挂起函数:
suspend fun <T> CompletableFuture<T>.await(): T
而在 CPS Transform
后,实际的代码是:
fun <T> CompletableFuture<T>.await(continuation: Continuation<T>): Any?
小结
- Kotlin协程,通过 状态机 实现,复用闭包。
- 挂起函数, 编译成 Continuation 回调对象,CPS。
suspend
以同步的编程方式,执行异步方法
文档
- Coroutine | Wikipedia
- KEEP | Kotlin
- KotlinConf 2017 - Deep Dive into Coroutines on JVM
- ContinuationImpl.kt
- 为什么无栈协程不能被非协程函数嵌套调用? | 知乎
- 浅谈有栈协程与无栈协程 | 知乎
- 理解有栈无栈协程