CPS 变换与状态机
源:Coroutines design document (KEEP)
Kotlin 协程之所以能在没有原生 VM 支持的情况下实现挂起,全靠编译器的 CPS (Continuation Passing Style) 变换。
什么是 CPS?
CPS 即“续体传递风格”。简单来说,编译器会将挂起函数的签名进行转换:
kotlin
suspend fun fetchUser(id: String): Userkotlin
// 返回值变成了 Object,因为它可能返回数据,也可能返回挂起标志
fun fetchUser(id: String, completion: Continuation<User>): Any?每一个 suspend 函数都会额外增加一个 Continuation 参数,这个参数本质上就是该函数运行后的“余下逻辑(续体)”。
状态机生成的原理
编译器会将协程体内的逻辑根据挂起点拆分成多个代码块,并生成一个 label 变量来记录当前执行到哪一步。
状态拆解示例
假设有如下代码:
kotlin
suspend fun demo() {
println("Start")
val res = request() // 挂起点
println("Result: $res")
}编译器生成的逻辑类似于:
查看状态机伪代码
java
class DemoContinuation extends ContinuationImpl {
int label = 0;
Object result;
@Override
public Object invokeSuspend(Object result) {
switch (this.label) {
case 0:
println("Start");
this.label = 1;
// 调用 request 并传入自己 (this)
Object maybeSuspended = request(this);
if (maybeSuspended == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED;
case 1:
// 恢复执行
println("Result: " + result);
return Unit.INSTANCE;
}
}
}Continuation:续体对象
Continuation 是协程状态的容器,它包含:
coroutineContext: 协程上下文。resumeWith(result): 用于恢复协程执行的方法。
挂起与恢复的闭环
- 挂起:函数返回一个特殊的单例
COROUTINE_SUSPENDED。 - 退出:线程退出当前的
invokeSuspend执行。 - 完成任务:异步操作(如 OkHttp 拦截器)完成后,拿到结果。
- 恢复:调用
continuation.resume(result),重新触发invokeSuspend,此时label已更新,进入下一个 case。
性能考量:分配与开销
虽然 CPS 引入了额外的对象分配(每个挂起函数调用链都会生成或重用 Continuation),但 Kotlin 编译器进行了大量优化:
- 状态机重用:在同一个协程内,多个挂起点共享同一个 Continuation 实例。
- 内联优化:对于非挂起的路径,尽可能减少分支开销。
核心准则总结
- 理解 label 是协程的“进度条”:它决定了恢复时从哪一行代码开始执行。
- 挂起函数是对象而非纯函数:在底层,它们是带有状态的类成员。
- 不要试图手动模拟 CPS:这是编译器的工作,手动操作极易破坏协程的原子性。