挂起和恢复的内部机制

状态机到底长什么样?——从手写 state 到编译器生成的 Continuation

状态机 Continuation COROUTINE_SUSPENDED 编译器魔法 局部变量堆化

目录导航(点击跳转)

一、手写一个「伪协程」状态机

1 你希望这样写(顺序代码)
// 你希望这样写:
void loginAndFetch() {
    String token = requestToken("user", "pass");   // ① 网络请求,慢
    String data  = fetchData(token);                // ② 又一个网络请求
    show(data);                                     // ③ 展示
}

三行代码,清晰直观。但每一行都可能要等网络,如果按普通函数写,线程就卡住了。

2 编译器实际上帮你生成差不多这样的东西
class LoginAndFetchStateMachine {
    int state = 0;          // 当前跑到哪一步
    String token;           // 局部变量 token → 变成字段
    String data;            // 局部变量 data → 变成字段

    void resume() {
        switch (state) {
            case 0:
                // ↓ 开始 ①:发起网络请求
                state = 1;
                httpClient.sendAsync("POST", "/login", "user:pass",
                    result -> {
                        this.token = result;  // 结果存进字段
                        this.resume();         // 回来继续
                    });
                return;  // ← 暂停!人走了,但 token, state 留在堆上

            case 1:
                // ↓ 网络请求回来了,接着跑 ②
                state = 2;
                httpClient.sendAsync("GET", "/data?token=" + this.token,
                    result -> {
                        this.data = result;
                        this.resume();
                    });
                return;  // ← 又暂停!

            case 2:
                // ↓ 都拿到了
                show(this.data);
                return;
        }
    }
}
3 时间线:三次 resume,三段执行
调用者 状态机 网络
1 调用 resume() → state=0 → 发出请求① → state→1 → return(暂停)
线程去干别的。token 和 state=1 留在堆上的状态机对象里。
2 网络在传输,协程在睡觉...
请求①返回,回调里调 resume()
3 token 被赋值 → state=1 → 发出请求② → state→2 → return(又暂停)
线程又去干别的。token、data、state=2 继续在堆上。
4 网络又在传输...
请求②返回,回调里调 resume()
5 data 被赋值 → state=2 → show(data) → 结束

你眼睛看到的:顺序三行。计算机实际跑的:三个 case,每次跑一个就出去了。

二、Kotlin 编译器真实产物:Continuation

1 Kotlin 代码
suspend fun loginAndFetch() {
    val token = requestToken("user", "pass")   // suspend 函数
    val data  = fetchData(token)                // suspend 函数
    show(data)
}
2 编译器把它变成一个 Continuation(续体)

Continuation是 Kotlin 里状态机的官方叫法——「续体」,意思是"待续的执行体"。

// 编译器生成(伪代码,展示核心结构)
class LoginAndFetchContinuation implements Continuation {
    int label = 0;
    Object result;           // 上一个 suspend 函数的返回值
    Object token;            // 局部变量 → 字段
    Object data;             // 局部变量 → 字段

    @Override
    Object invokeSuspend(Object result) {
        this.result = result;

        switch (label) {
            case 0:
                // val token = requestToken("user", "pass")
                label = 1;
                Object r1 = requestToken("user", "pass", this);  // ← this = 续体!
                if (r1 == COROUTINE_SUSPENDED) {                 // ← 如果返回了暂停标记
                    return COROUTINE_SUSPENDED;                   //    那就真暂停
                }
                // 没暂停,往下走(fall through)

            case 1:
                token = result;     // requestToken 的结果(通过 result 传进来了)
                label = 2;
                Object r2 = fetchData(token, this);
                if (r2 == COROUTINE_SUSPENDED) {
                    return COROUTINE_SUSPENDED;
                }

            case 2:
                data = result;
                show(data);
                return Unit.INSTANCE;  // 完成
        }
    }
}
3 Continuation 接口的真面目

Kotlin 标准库中 Continuation 的真实定义:

// Kotlin 标准库源码
public interface Continuation<in T> {
    // 续体所在的协程上下文
    public val context: CoroutineContext

    // 恢复续体:传入上一个挂起函数的结果
    public fun resumeWith(result: Result<T>)
}

每个 suspend 函数编译后都会多一个 Continuation参数,这个参数就是调用者的续体——「我跑完了把结果交给谁」。

// 你写的:
suspend fun requestToken(user: String, pass: String): String

// 编译后(伪代码):
fun requestToken(user: String, pass: String, continuation: Continuation): Any?
//                                                          ↑ 多了这个参数
// 返回 Any? 因为可能返回实际结果(String)或 COROUTINE_SUSPENDED

三、关键概念:COROUTINE_SUSPENDED

1 一个特殊的标记值

COROUTINE_SUSPENDED是一个单例标记对象,定义在 Kotlin 标准库中:

// Kotlin 标准库中的定义
@SinceKotlin("1.3")
public val COROUTINE_SUSPENDED: Any get() = CoroutineSingletons.COROUTINE_SUSPENDED

internal enum class CoroutineSingletons {
    COROUTINE_SUSPENDED,
    UNDECIDED,
    RESUMED
}

suspend 函数返回两种东西

情况一:返回实际结果

数据已经到了(缓存命中 / 已经准备好了)

→ 返回真实数据,调用者直接继续往下走

没有真正挂起

情况二:返回 COROUTINE_SUSPENDED

还没好,我先挂起来了,回头叫你

→ 返回这个标记,调用者一路 return 出去

真挂起,线程去干别的

2 调用 suspend 函数的完整判断逻辑
调用 suspend 函数
传入续体 this
检查返回值
= COROUTINE_SUSPENDED
→ 真挂起
→ return(线程走人)
→ 等数据到了回调 resume
→ invokeSuspend(数据) 被调
→ 从下一个 case 继续
= 实际数据
→ 没挂起
→ 直接继续往下走
→ 不经过 resume 回调
→ 同一个线程直接跑

四、完整流程串一遍

1 从调用到完成的完整路径
调用者 Continuation 网络层
用户代码调用 loginAndFetch()
1 invokeSuspend(null) → label=0
2 调用 requestToken("user","pass", this=续体)
3 requestToken 发起 HTTP 请求
返回 COROUTINE_SUSPENDED → 协程挂起 → 线程被释放去跑别的协程
4 ... 网络在传输,协程在睡觉 ...
HTTP 响应到了 → 回调触发
5 continuation.resumeWith(tokenResult)
6 → invokeSuspend(tokenResult) → label=1
7 token = result → 调用 fetchData(token, this)
又返回 COROUTINE_SUSPENDED → 又挂起 → 线程又去干别的
8 ... 网络又在传输 ...
数据到了 → 回调触发
9 continuation.resumeWith(dataResult)
10 → invokeSuspend(dataResult) → label=2
11 data = result → show(data) → 返回 Unit(完成)

五、局部变量去哪了?

1 普通函数 vs 协程
普通函数:局部变量在栈上
 │ main()
 ├─ handler()
 │  ├─ int token = ... ← 栈局部变量
 │  │  函数结束,token 销毁
 │  └─
 │
函数结束 → 栈帧弹出 → 变量消失
协程:局部变量变成状态机类的字段
┌─────────────────────────┐
│ LoginAndFetchContinuation│
│  int label = 1         │
│  Object token = "abc"  │
│  Object data = null    │
└─────────────────────────┘
     ↑  全在堆上,持久存在
挂起多少次都在 → 堆上的对象不会被回收
2 为什么必须放在堆上?

因为协程挂起后线程就离开了。如果局部变量在栈上,线程一走栈帧弹出,变量就没了。

只有放在堆上的对象里,线程离开后变量才能继续存活,等下次线程(可能是另一个线程)回来继续用。

这就是「闭包捕获」的升级版:编译器把所有跨挂起点的局部变量全部提升为类的成员字段。这就是为什么 Kotlin 协程里你完全不需要关心变量生命周期——编译器全替你处理好了。

六、编译器到底生成了什么(反编译验证)

1 源码 → 字节码 → Java 反编译

如果你把 Kotlin 的 suspend 函数编译后反编译成 Java,你会看到:

// 反编译后的 Java 伪代码(简化)
public final class LoginAndFetchKt {
    public static Object loginAndFetch(Continuation $continuation) {
        // 类型转换,拿到状态机对象
        LoginAndFetchContinuation cont = (LoginAndFetchContinuation) $continuation;

        switch (cont.label) {
            case 0:
                // 第一个挂起点之前
                cont.label = 1;
                Object result = requestToken("user", "pass", cont);
                if (result == Intrinsics.COROUTINE_SUSPENDED) {
                    return Intrinsics.COROUTINE_SUSPENDED;
                }
                // fall through

            case 1:
                // token 拿到,继续
                cont.token = (String) cont.result;
                cont.label = 2;
                Object result2 = fetchData(cont.token, cont);
                if (result2 == Intrinsics.COROUTINE_SUSPENDED) {
                    return Intrinsics.COROUTINE_SUSPENDED;
                }
                // fall through

            case 2:
                cont.data = (String) cont.result;
                show(cont.data);
                return Unit.INSTANCE;
        }
    }

    // 编译器生成的状态机内部类
    static final class LoginAndFetchContinuation extends ContinuationImpl {
        Object token;   // 局部变量"提升"为字段
        Object data;    // 局部变量"提升"为字段
        int label;
    }
}

关键观察:

  • suspend 函数多了一个 Continuation参数
  • 返回类型变成 Object(因为可能返回 COROUTINE_SUSPENDED 或实际值)
  • 所有跨挂起点的局部变量变成类的字段
  • switch-case 实现状态跳转
  • 每个挂起点检查返回值是否为 COROUTINE_SUSPENDED

七、核心思想总结

第三章核心要点

一句话总结第三章

协程 = 编译器把你的函数切成 N 个 case + 每个 suspend 调用都是挂起点,调用时把续体(this)传下去 + 返回 COROUTINE_SUSPENDED → 真挂起,线程走人 + 数据到了 → 回调调续体 → 从下一个 case 继续 + 所有局部变量变成类字段,活在堆上,不怕暂停。