Skip to content

CPS 变换与状态机

源:Coroutines design document (KEEP)

Kotlin 协程之所以能在没有原生 VM 支持的情况下实现挂起,全靠编译器的 CPS (Continuation Passing Style) 变换

什么是 CPS?

CPS 即“续体传递风格”。简单来说,编译器会将挂起函数的签名进行转换:

kotlin
suspend fun fetchUser(id: String): User
kotlin
// 返回值变成了 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): 用于恢复协程执行的方法。

挂起与恢复的闭环

  1. 挂起:函数返回一个特殊的单例 COROUTINE_SUSPENDED
  2. 退出:线程退出当前的 invokeSuspend 执行。
  3. 完成任务:异步操作(如 OkHttp 拦截器)完成后,拿到结果。
  4. 恢复:调用 continuation.resume(result),重新触发 invokeSuspend,此时 label 已更新,进入下一个 case。

性能考量:分配与开销

虽然 CPS 引入了额外的对象分配(每个挂起函数调用链都会生成或重用 Continuation),但 Kotlin 编译器进行了大量优化:

  • 状态机重用:在同一个协程内,多个挂起点共享同一个 Continuation 实例。
  • 内联优化:对于非挂起的路径,尽可能减少分支开销。

核心准则总结

  1. 理解 label 是协程的“进度条”:它决定了恢复时从哪一行代码开始执行。
  2. 挂起函数是对象而非纯函数:在底层,它们是带有状态的类成员。
  3. 不要试图手动模拟 CPS:这是编译器的工作,手动操作极易破坏协程的原子性。