协程suspend 如何被编译成“状态机”

1) 大图景:CPS 改写 + 堆上栈(stackless)

  • 思想 :suspend 会被编译器做 Continuation-Passing Style (CPS) 转换------

    原本"直接返回"的函数,被改写成"把结果回调给一个 Continuation"。

  • 结果 :每个含挂起点的 suspend 函数/λ 会被"状态机化":

    • 局部变量、下一步要从哪行继续(label)都放进生成的对象里(堆上保存"栈帧")。

    • 真正"继续执行"靠调用 Continuation.resumeWith(result),像是把程序计数器跳回 switch(label) 的某一分支。

Continuation(简化):

kotlin 复制代码
interface Continuation<in T> {
    val context: CoroutineContext
    fun resumeWith(result: Result<T>)
}

2) 签名改写:隐式多了一个 Continuation 参数

示例(源代码):

kotlin 复制代码
suspend fun foo(a: Int): String {
    delay(100)
    return a.toString()
}

编译后(概念化伪代码):

javascript 复制代码
Object foo(int a, Continuation<? super String> cont) {
    // 可能立即返回值,也可能返回 COROUTINE_SUSPENDED
}
  • 返回类型被抹成 Object,用来承载 真实结果COROUTINE_SUSPENDED
  • 没有挂起点 时,仍会多一个 Continuation 形参,但实现通常是直返(不建状态机)。

3) 生成的"状态机类":ContinuationImpl/SuspendLambda

两种常见形态:

  1. 挂起 λ(suspend {}) :生成类继承 SuspendLambda,实现 FunctionN,核心逻辑在 invokeSuspend()。

  2. 挂起函数 :编译器会为它生成一个内部 ContinuationImpl 子类,封装局部变量、label、异常等;继续执行时也走 invokeSuspend()。

典型骨架(伪代码):

typescript 复制代码
final class Foo$1 extends ContinuationImpl {
    int label;           // 状态机的程序计数器
    Object L$0, L$1;     // 若干个槽保存局部变量/参数
    Object result;       // 上一次 resumeWith 传入的结果/异常

    Foo$1(Continuation parent) { super(parent); }

    public Object invokeSuspend(Object param) {
        this.result = param;
        switch (label) {
            case 0: {
                // 初次进入
                // 保存局部,设置下一状态
                label = 1;
                Object r = delay(100, this);
                if (r == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED;
                // fallthrough 到状态1逻辑
            }
            case 1: {
                // 从 delay 返回后继续
                int a = (int) L$0;
                return a + ""; // 返回最终结果
            }
            default:
                throw new IllegalStateException("call to 'resume' before 'invoke'");
        }
    }
}

要点

  • 每遇到一个挂起点 ,就把需要的活跃局部写入 L$* 槽,设置 label = 下一状态,然后调用下游挂起函数。
  • 如果下游返回 COROUTINE_SUSPENDED,就立刻把 COROUTINE_SUSPENDED 原样向上返回;真正的恢复发生在以后某个 resumeWith。

4) 挂起点如何"切片"

以 delay(100) 为例(简化):

ini 复制代码
label = 1
L$0 = a
val r = delay(100, this)
if (r == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
// 否则继续向下执行(说明立刻完成,没有真的挂起)

恢复时序

  1. 定时器到点 → 调度器拿到原 Continuation → 调 resumeWith(Result.success(Unit));
  2. 跳回 invokeSuspend(),result = Unit,switch(label) 命中 case 1;
  3. 从 L$0 里把 a 读回,继续执行。

5)COROUTINE_SUSPENDED与resumeWith

  • COROUTINE_SUSPENDED 是个哨兵对象:表示"我挂起了,别往下跑了,等我 resumeWith 再说"。

  • resumeWith(Result) 里封装成功值异常

    • 成功:Result.success(value) → 下一次 invokeSuspend(param=value);
    • 失败:Result.failure(e) → 下一次 invokeSuspend(param=Failure(e)),随后 throw 出来或在 try/catch 中处理。

6) 调度器/拦截器:ContinuationInterceptor

  • Continuation 在恢复前会经 intercepted() 包一层(例如 DispatchedContinuation),由 ContinuationInterceptor (如 Dispatchers.Main/IO)控制在哪个线程恢复。

  • 构建器 launch/async 会创建一个起始 Continuation (如 ScopeCoroutine/StandaloneCoroutine),并把 block 的 Continuation 链接起来形成"堆上调用栈"。

启动路径(概念):

scss 复制代码
block.startCoroutine(receiver, completion)                 // 或
block.startCoroutineUninterceptedOrReturn(completion)      // 内联优化路径

7) 取消/异常如何穿透状态机

  • 取消本质是向上游 抛出 CancellationException ,所有挂起点都是可取消的"安全点"。

  • 你的 try/finally 会被拆成状态机的多个 label,从而保证 finally 一定运行(无论正常返回、异常还是取消)。

示例(保证释放):

kotlin 复制代码
suspend fun use() {
    val r = acquire()
    try {
        delay(1000)   // 这里若取消,会在恢复路径里跳到 finally
    } finally {
        r.close()
    }
}

8) inline/无挂起点/尾调用 等细节

  • inline suspend 函数 :主体会内联到调用处;若内部仍有挂起点,状态机依旧存在,但"归属"到调用处的那一套 Continuation。
  • 无挂起点的 suspend :虽然签名多了 Continuation,但不会生成状态机,通常直接计算后返回结果。
  • 尾部挂起优化:编译器/标准库会尽量通过 startCoroutineUninterceptedOrReturn 走更少包装,减少切换成本。
  • label | Int.MIN_VALUE 位标记 :反编译常见到把最高位 OR 上,表示"这是恢复路径"(debug/probe 用),理解为恢复态标识即可。

9) 与 Flow/Channel 的衔接(心智模型)

  • suspend 是基本指令;Flow 运算符本身也是一堆 suspend 挂起点串起来的状态机。
  • Channel 在 send/receive 处会挂起;因此它们都依赖上述"保存现场→返回 COROUTINE_SUSPENDED→稍后 resumeWith"的流程。

10) 一个完整示例(源代码 → 伪反编译)

源:

kotlin 复制代码
suspend fun twoDelays(a: Int): String {
    delay(10)
    val x = a + 1
    delay(20)
    return "v=$x"
}

伪反编译(高度简化):

typescript 复制代码
Object twoDelays(int a, Continuation<? super String> cont) {
    class SM extends ContinuationImpl {
        int label;
        Object result;
        int L$0;  // 保存 a
        int L$1;  // 保存 x
        SM(Continuation parent) { super(parent); }
        public Object invokeSuspend(Object param) {
            result = param;
            switch (label) {
                case 0: {
                    label = 1;
                    L$0 = a;
                    if (delay(10, this) == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED;
                }
                case 1: {
                    int x = L$0 + 1;
                    L$1 = x;
                    label = 2;
                    if (delay(20, this) == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED;
                }
                case 2: {
                    return "v=" + L$1;
                }
            }
            throw new IllegalStateException();
        }
    }
    SM sm = (cont instanceof SM) ? (SM) cont : new SM(cont);
    return sm.invokeSuspend(Unit.INSTANCE);
}

观察点:两个挂起点 → 三段代码块 → 两次设置 label 与保存局部。


11) 实战调试/读懂状态机的小贴士

  • 反编译看 invokeSuspend 的 switch(label):每个 case 即一个"继续点"。
  • 查看是否存在 COROUTINE_SUSPENDED 的快速返回;它就是"真挂起"的判定。
  • L$* 槽里是被拆散的局部;label 是当下所在阶段。
  • 若 finally 没执行,大概率是你在挂起函数外侧做了资源管理(应把清理放到 suspend/withContext 封闭的作用域里)。

一句话总结

suspend = 把"同步栈"拆成"堆上状态机 + Continuation 回调链"

进入挂起点前存档(局部变量 + 下一步的 label),返回 COROUTINE_SUSPENDED;

等到外界 resumeWith,再按 label 读档,在正确的线程由调度器继续执行。

相关推荐
一直_在路上4 小时前
Go架构师实战:玩转缓存,击破医疗IT百万QPS与“三大天灾
前端·面试
怪兽20145 小时前
谈一谈Java成员变量,局部变量和静态变量的创建和回收时机
android·面试
王嘉俊9256 小时前
Java面试宝典:核心基础知识精讲
java·开发语言·面试·java基础·八股文
南北是北北6 小时前
Kotlin Channel 开箱即用
面试
前端缘梦6 小时前
前端模块化详解:CommonJS 与 ES Module 核心原理与面试指南
前端·面试·前端工程化
Hilaku7 小时前
前端开发,为什么容易被边缘化?
前端·javascript·面试
召摇8 小时前
命令-查询分离原则(Command-Query Separation)
前端·javascript·面试
知其然亦知其所以然8 小时前
MySQL 社招必考题:如何优化 UNION 查询?
后端·mysql·面试
怪兽20148 小时前
JRE、JDK、JVM 及 JIT 之间有什么不同?
java·面试