协程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 读档,在正确的线程由调度器继续执行。

相关推荐
java_nnnn2 小时前
面试题(网络部分前传)-13问
服务器·网络·网络协议·tcp/ip·面试
找方案10 小时前
AI+人力资源:AI招聘面试的兴起与争议
人工智能·面试·职场和发展
CC数分17 小时前
2026年HRBP岗位硬性要求和加分项
面试·职场和发展·数据分析
2601_9626802217 小时前
数据分析面试常考的SQL题和业务题分别是哪几类?
sql·面试·数据分析
黄敬峰17 小时前
一文搞懂 Agentic RAG:用 LangGraph 打造会思考的智能检索架构
面试
晚安日记wanna18 小时前
SSR 为什么不适合登录态从水合冲突到缓存串号
前端·react.js·面试
晚安日记wanna18 小时前
压测 TPS 卡在 800CPU 只跑四成六步定位法
运维·面试·测试
晚安日记wanna18 小时前
批量请求失败只弹一个 Toast面试官想听五层
前端·面试·架构
晚安日记wanna18 小时前
TS const 类型参数一道面试题的四层追问
前端·面试·typescript
晚安日记wanna18 小时前
大表 DDL 面试翻车现场Online DDL 为什么还会锁死业务
数据库·面试·架构