从"怎么写"到"编译器把它变成什么",再到"运行时怎么调度"的完整知识文档。
目录
- 第一部分:入门
- [1.1 什么是 suspend 函数](#1.1 什么是 suspend 函数)
- [1.2 最简单的 suspend 示例](#1.2 最简单的 suspend 示例)
- [1.3 suspend 的调用限制](#1.3 suspend 的调用限制)
- [第二部分:一个 suspend 里调用多个 suspend](#第二部分:一个 suspend 里调用多个 suspend)
- [2.1 串行调用](#2.1 串行调用)
- [2.2 并发调用](#2.2 并发调用)
- [2.3 串行 vs 并发对比](#2.3 串行 vs 并发对比)
- [第三部分:编译原理------suspend 被编译成什么](#第三部分:编译原理——suspend 被编译成什么)
- [3.1 CPS 转换](#3.1 CPS 转换)
- [3.2 状态机](#3.2 状态机)
- [3.3 局部变量提升为字段](#3.3 局部变量提升为字段)
- [3.4 多个挂起点:N+1 个状态](#3.4 多个挂起点:N+1 个状态)
- [3.5 关键澄清:状态机是同步的,异步性来自挂起/恢复](#3.5 关键澄清:状态机是同步的,异步性来自挂起/恢复)
- 第四部分:运行时------谁调用、怎么进入线程
- [4.1 runBlocking 做了什么](#4.1 runBlocking 做了什么)
- [4.2 完整时序:loadAll 被进入三次](#4.2 完整时序:loadAll 被进入三次)
- [4.3 结果如何向上传递](#4.3 结果如何向上传递)
- [第五部分:Dispatchers.IO 场景------多线程跳转](#第五部分:Dispatchers.IO 场景——多线程跳转)
- 第六部分:挂起点前后有普通代码会怎样
- 第七部分:总结与速查表
第一部分:入门
1.1 什么是 suspend 函数
suspend 是 Kotlin 用来标记可以被挂起的函数的修饰符。它的特点:
- 可以在不阻塞线程的情况下暂停执行
- 只能在协程 或其他 suspend 函数中调用
- 挂起后,线程会被释放去干别的事,之后在某个时机"恢复"
1.2 最简单的 suspend 示例
kotlin
import kotlinx.coroutines.*
suspend fun fetchData(): String {
delay(1000) // 挂起 1 秒,不阻塞线程
return "数据加载完成"
}
fun main() = runBlocking {
println("开始加载...")
val result = fetchData()
println(result)
}
输出:
开始加载...
(等待 1 秒)
数据加载完成
关键点:
| 概念 | 说明 |
|---|---|
suspend |
标记可挂起函数 |
delay() |
挂起函数,不阻塞线程 (区别 Thread.sleep) |
runBlocking |
桥接阻塞世界与协程世界,常用于 main |
| 依赖 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") |
1.3 suspend 的调用限制
kotlin
suspend fun a(): String = "a"
fun normalFun() {
// ❌ 编译错误:Suspend function 'a' should be called only from a coroutine
// a()
}
规则:suspend 函数只能被 suspend 函数或协程调用。
第二部分:一个 suspend 里调用多个 suspend
2.1 串行调用
kotlin
suspend fun fetchUser(): String {
delay(1000)
return "用户信息"
}
suspend fun fetchOrder(): String {
delay(1000)
return "订单信息"
}
// 串行:一个执行完再执行下一个
suspend fun loadAll(): String {
val user = fetchUser() // 约 1 秒
val order = fetchOrder() // 约 1 秒
return "$user + $order"
}
总耗时约 2000ms。
2.2 并发调用
kotlin
suspend fun loadAllConcurrent(): String = coroutineScope {
val userDeferred = async { fetchUser() }
val orderDeferred = async { fetchOrder() }
val user = userDeferred.await()
val order = orderDeferred.await()
"$user + $order"
}
总耗时约 1000ms。
简化写法:
kotlin
suspend fun loadAllSimple(): String = coroutineScope {
val results = listOf(
async { fetchUser() },
async { fetchOrder() }
).awaitAll()
results.joinToString(" + ")
}
2.3 串行 vs 并发对比
| 方式 | 写法 | 耗时 | 适用场景 |
|---|---|---|---|
| 串行 | 依次调用 | 两者之和 | 后一个依赖前一个 |
| 并发 | async { } + await() |
较慢的那个 | 两个任务相互独立 |
注意 :async{}.await() 再 async{}.await() 会退化成串行。必须先启动所有 async,再统一 await。
异常处理:
kotlin
suspend fun loadAllSafe(): String = coroutineScope {
val user = async { runCatching { fetchUser() }.getOrDefault("失败") }
val order = async { runCatching { fetchOrder() }.getOrDefault("失败") }
"${user.await()} + ${order.await()}"
}
⚠️
async失败会取消整个coroutineScope。想独立处理异常,用supervisorScope或runCatching。
第三部分:编译原理------suspend 被编译成什么
3.1 CPS 转换
suspend 函数会被编译成 CPS(Continuation Passing Style) 形式:多一个 Continuation 参数,返回值变成 Object。
kotlin
suspend fun loadAll(): String
编译后签名大致是:
java
Object loadAll(Continuation<? super String> continuation)
Continuation 接口:
java
public interface Continuation<T> {
void resumeWith(Object result); // result 是 T 或 Throwable
CoroutineContext getContext();
}
Object 返回值有两种可能:
- 正常结果(最终 String)
COROUTINE_SUSPENDED特殊标记(表示"我真的挂起了")
3.2 状态机
编译器把 loadAll 变成一个 switch + label 状态机。以 2.1 的串行版本为例:
kotlin
suspend fun loadAll(): String {
val user = fetchUser() // 调用点 1
val order = fetchOrder() // 调用点 2
return "$user + $order"
}
编译后等价 Java:
java
public static Object loadAll(Continuation<? super String> completion) {
LoadAllContinuation c = new LoadAllContinuation(completion);
Object result;
switch (c.label) {
case 0:
c.label = 1; // 下次从 case 1 恢复
result = fetchUser(c); // ★ 调用点 1
if (result == COROUTINE_SUSPENDED) {
return COROUTINE_SUSPENDED; // ★ 挂起,栈弹出
}
break; // 没挂起,继续
case 1:
result = c.result; // 取回 user
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke'");
}
String user = (String) result;
c.user = user; // ★ 提升为字段
switch (c.label) {
case 1:
c.label = 2; // 下次从 case 2 恢复
result = fetchOrder(c); // ★ 调用点 2
if (result == COROUTINE_SUSPENDED) {
return COROUTINE_SUSPENDED;
}
break;
case 2:
result = c.result; // 取回 order
break;
}
String order = (String) result;
c.order = order;
return c.user + " + " + c.order; // ★ 真正返回
}
状态机 Continuation:
java
final class LoadAllContinuation implements Continuation<Object> {
int label = 0;
Object result;
String user; // ★ 局部变量提升
String order; // ★ 局部变量提升
final Continuation<? super String> completion;
LoadAllContinuation(Continuation<? super String> completion) {
this.completion = completion;
}
@Override
public void resumeWith(Object result) {
this.result = result;
ExampleKt.loadAll(this); // ★ 重新进入
}
@Override
public CoroutineContext getContext() {
return completion.getContext();
}
}
三次进入 loadAll,label 不同:
| 进入时机 | label | 走哪个 case | 行为 |
|---|---|---|---|
| 首次调用 | 0 | case 0 |
调用 fetchUser |
| fetchUser 完成后 | 1 | case 1 |
取回 user,调用 fetchOrder |
| fetchOrder 完成后 | 2 | case 2 |
取回 order,返回结果 |
3.3 局部变量提升为字段
为什么? 一旦挂起(return),栈帧销毁,局部变量消失;恢复后还要用,所以必须提升为 Continuation 的字段。
java
c.user = user; // 挂起前保存
// ... 恢复后 ...
return c.user + " + " + c.order; // 从字段取回
判断标准 :变量在某个挂起点之后是否还会被用到?会 → 提升;只在单段内用 → 保留为局部变量。
3.4 多个挂起点:N+1 个状态
N 个挂起点 → 有 N+1 个状态(label = 0...N)→ 代码被切成 N+1 段。
label 0 ──[普通代码]──[挂起点1]──► label 1
label 1 ──[普通代码]──[挂起点2]──► label 2
label 2 ──[普通代码]──[return]
3.5 关键澄清:状态机是同步的,异步性来自挂起/恢复
这是最容易误解的一点!
协程的状态机代码本身是同步的 ------就是一个 switch + label。真正让它"异步"的是:
- 挂起时直接
return COROUTINE_SUSPENDED,把控制权交还调用者(线程被释放) delay/ IO 完成后 ,由另一个线程或调度器 调用resumeWith,重新进入这个函数
所以"异步"体现在 谁在什么时候再次调用 loadAll,而不是函数体本身。
delay 的等价实现(关键在于不阻塞线程):
java
public static Object delay(long millis, Continuation<?> cont) {
Dispatcher dispatcher = cont.getContext().get(Dispatcher.Key);
dispatcher.scheduleAfter(millis, () -> {
// ⚠️ 1 秒后,由调度器线程执行
cont.resumeWith(Unit.INSTANCE);
});
return COROUTINE_SUSPENDED; // 立即返回,当前线程释放
}
回调版对照(更直观地体现异步):
java
// 协程状态机等价于这种"回调地狱"的拍平
public static void loadAllAsync(Callback<String> cb) {
fetchUserAsync(userResult -> {
fetchOrderAsync(orderResult -> {
cb.onResult(userResult + " + " + orderResult);
});
});
}
一句话 :状态机函数体是同步的;异步性来自 COROUTINE_SUSPENDED 的 return 和之后 resumeWith 的重新进入。
第四部分:运行时------谁调用、怎么进入线程
4.1 runBlocking 做了什么
runBlocking 是普通函数(不是 suspend),负责:
- 创建事件循环 (
BlockingEventLoop) - 把它作为协程上下文传给 lambda
- 在当前线程 上立即启动 lambda
- 若 lambda 挂起,就在当前线程跑事件循环,直到完成
java
public static <T> T runBlocking(Function1<Continuation<? super T>, Object> block) {
BlockingEventLoop loop = new BlockingEventLoop();
Continuation<T> completion = new Continuation<T>() {
public void resumeWith(Object r) { loop.complete(r); }
public CoroutineContext getContext() { return loop; }
};
Object r = block.invoke(completion); // ★ 当前线程直接调用
if (r != COROUTINE_SUSPENDED) return (T) r;
return (T) loop.runUntilComplete(); // ★ 挂起了,跑事件循环
}
4.2 完整时序:loadAll 被进入三次
═══════ 阶段 0:main 线程启动 ═══════
main 线程
main() → runBlocking(block)
└─ block.invoke(completion)
└─ loadAll(c) ← ★ 第一次进入,main 线程
case 0: fetchUser(c) → delay → 注册定时任务 → SUSPENDED
return SUSPENDED
└─ loop.runUntilComplete() ← main 线程进入事件循环,park
═══════ 阶段 1:1 秒后,定时器触发 ═══════
定时器线程
sleep(1000) 结束
→ loop.enqueue(() -> c.resumeWith(userResult))
═══════ 阶段 2:main 线程取出任务 ═══════
main 线程(还在 loop 里)
task.run()
└─ c.resumeWith(userResult)
└─ loadAll(c) ← ★ 第二次进入,main 线程
case 1: 取回 user, fetchOrder(c) → SUSPENDED
return SUSPENDED
└─ 继续 while,park
═══════ 阶段 3:再 1 秒后 ═══════
定时器线程:enqueue(resumeWith)
main 线程:
task.run()
└─ c.resumeWith(orderResult)
└─ loadAll(c) ← ★ 第三次进入,main 线程
case 2: 取回 order
return "用户信息 + 订单信息"
└─ completion.resumeWith(...)
└─ loop.complete(...)
└─ while 退出,runBlocking 返回
"谁调用了 loadAll":
| 第几次进入 | 谁调用 | 在哪个线程 | 走哪个 case |
|---|---|---|---|
| 第 1 次 | main lambda 的 invoke |
main | case 0 |
| 第 2 次 | LoadAllContinuation.resumeWith |
main | case 1 |
| 第 3 次 | LoadAllContinuation.resumeWith |
main | case 2 |
"怎么进入线程":
- 第一次 :main 线程同步调用链
runBlocking → block.invoke → loadAll - 挂起后 :
loadAllreturn,栈弹出,main 线程跑事件循环 - 恢复时 :定时器线程入队 任务,main 线程取出执行 ,重新进入
loadAll
runBlocking场景下没有新线程参与业务逻辑,全程 main 线程,只是执行时机被切成三段。
4.3 结果如何向上传递
case 2 拿到 order 后,返回值同步 沿 resumeWith 链向上冒泡:
java
// LoadAllContinuation.resumeWith
public void resumeWith(Object result) {
this.result = result;
Object r = ExampleKt.loadAll(this); // 第三次进入,返回最终字符串
if (r != COROUTINE_SUSPENDED) {
this.completion.resumeWith(r); // ★ 往外传
}
}
// MainContinuation.resumeWith(main lambda)
public void resumeWith(Object result) {
this.result = result;
Object r = ExampleKt.main$lambda(this); // 重新进入 main lambda
if (r != COROUTINE_SUSPENDED) {
runBlockingCompletion.resumeWith(r);
}
}
// runBlockingCompletion.resumeWith
public void resumeWith(Object result) {
loop.complete(result); // ★ 标记完成,唤醒 main 线程
}
完整链路:
loadAll 返回 "用户信息 + 订单信息"
→ LoadAllContinuation.resumeWith 往外传
→ MainContinuation.resumeWith(println 打印)
→ runBlockingCompletion.resumeWith
→ loop.complete(...) 置 completed = true
→ runUntilComplete 的 while 退出
→ runBlocking 返回
→ main 线程解除阻塞,main 结束
第五部分:Dispatchers.IO 场景------多线程跳转
5.1 代码
kotlin
suspend fun loadAll(): String = withContext(Dispatchers.IO) {
val user = fetchUser()
val order = fetchOrder()
"$user + $order"
}
5.2 withContext 干了什么
- 切上下文 :把后续代码的
CoroutineContext换成IO - 调度到 IO 线程池 :
Dispatchers.IO背后是共享线程池(默认最多 64 线程) - 执行 block:在 IO 线程池的某个线程上跑
- 结果传回 :完成后
resumeWith恢复回原上下文(这里是 main)
5.3 完整时序:跨三个 IO 线程
═══════ 阶段 0:main 线程启动 ═══════
main 线程
runBlocking → block.invoke → loadAll(c)
└─ withContext(IO) { ... }
→ 把 block 调度到 IO 线程池
→ 返回 SUSPENDED ← main 线程释放
└─ runBlocking 进入事件循环,park
═══════ 阶段 1:IO 线程执行 block 开头 ═══════
IO-1 线程
loadAll$block(c) ← ★ 第一次进入,IO-1
case 0:
fetchUser(c) → delay → SUSPENDED
→ IO-1 释放,回线程池
═══════ 阶段 2:1 秒后 ═══════
定时器线程
→ 投递 resumeWith 任务到 IO 线程池
IO-3 线程 ← ★ 可能是不同线程!
c.resumeWith(userResult)
└─ loadAll$block(c) ← ★ 第二次进入,IO-3
case 1:
fetchOrder(c) → delay → SUSPENDED
→ IO-3 释放
═══════ 阶段 3:再 1 秒后 ═══════
IO-2 线程 ← ★ 又是另一个线程!
c.resumeWith(orderResult)
└─ loadAll$block(c) ← ★ 第三次进入,IO-2
case 2:
return "用户信息 + 订单信息"
═══════ 阶段 4:切回 main ═══════
IO-2 线程
→ withContext 完成
→ 把结果 resumeWith 回原上下文(main 的 event loop)
main 线程
completion.resumeWith("用户信息 + 订单信息")
→ main lambda 的 case 1:println(...)
→ runBlocking 退出
5.4 对比表
| 维度 | runBlocking(无 withContext) |
withContext(Dispatchers.IO) |
|---|---|---|
| 执行线程 | 始终 main | IO 线程池任意线程 |
| 线程切换 | 无 | 每次挂起/恢复可能切换 |
| 并发能力 | 单线程事件循环 | 多线程并行 |
delay 恢复后 |
main 事件循环 | IO 线程池 |
| 适合场景 | 测试、简单示例 | 阻塞 IO(文件、网络) |
结论 :加了 Dispatchers.IO 后,loadAll 状态机不再固定在一个线程 ,三次进入可能落在三个不同的 DefaultDispatcher-worker-N 线程上;block 完成后再切回 main 打印。
第六部分:挂起点前后有普通代码会怎样
6.1 例子
kotlin
suspend fun loadAll(): String {
val prefix = "【" // ① 挂起前
println("开始加载") // ② 挂起前
val user = fetchUser() // ③ 挂起点 1
val mid = " | " // ④ 两个挂起之间
println("user 已拿到") // ⑤ 两个挂起之间
val order = fetchOrder() // ⑥ 挂起点 2
val suffix = "】" // ⑦ 挂起后
return prefix + user + mid + order + suffix // ⑧
}
6.2 核心规则
编译器只在
suspend调用点切case;普通代码按"属于哪一段"被分配到相邻的case里。
切成 3 段:
| 段 | label | 包含代码 |
|---|---|---|
| 段 1 | 0 |
①②③ |
| 段 2 | 1 |
④⑤⑥ |
| 段 3 | 2 |
⑦⑧ |
只有 3 个 case,不管中间多少行普通代码。
6.3 等价 Java(真实字节码形态)
真实产物是每个挂起点前一个小 switch:
java
public static Object loadAll(Continuation<? super String> completion) {
LoadAllContinuation c = new LoadAllContinuation(completion);
Object result;
switch (c.label) {
case 0:
c.prefix = "【"; // ①
System.out.println("开始加载"); // ②
c.label = 1;
result = fetchUser(c); // ③
if (result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED;
break;
case 1:
result = c.result; // 取回 user
break;
default:
throw new IllegalStateException();
}
String user = (String) result;
c.user = user;
c.mid = " | "; // ④
System.out.println("user 已拿到"); // ⑤
switch (c.label) {
case 1:
c.label = 2;
result = fetchOrder(c); // ⑥
if (result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED;
break;
case 2:
result = c.result; // 取回 order
break;
default:
throw new IllegalStateException();
}
String order = (String) result;
c.order = order;
String suffix = "】"; // ⑦
return c.prefix + c.user + c.mid + c.order + suffix; // ⑧
}
6.4 局部变量提升
java
final class LoadAllContinuation implements Continuation<Object> {
int label = 0;
Object result;
String prefix; // ★ 跨挂起点
String user; // ★ 跨挂起点
String mid; // ★ 跨挂起点
String order; // ★ 跨挂起点
// suffix 不提升:只在最后一段用,不跨挂起点
final Continuation<? super String> completion;
}
6.5 各种位置代码归属
| 代码位置 | 归属 | 是否提升字段 |
|---|---|---|
fetchUser 之前 |
case 0 |
跨挂起点使用 → 提升 |
| 两个挂起之间 | case 1 |
跨第二个挂起点使用 → 提升 |
fetchOrder 之后 |
case 2 |
一般不提升 |
纯语句 (如 println)不产生变量,不提升字段 ,直接留在对应 case 段。
6.6 try/catch 穿插
如果挂起点被包在 try 里,编译器会插入异常表 和额外的 label 分支,处理"从挂起点恢复时抛异常"的情况------这是 COROUTINE_SUSPENDED 之外的第二种返回路径 (返回 Result.failure)。
kotlin
suspend fun loadAll(): String {
val user = try {
fetchUser()
} catch (e: Exception) {
"默认用户"
}
// ...
}
第七部分:总结与速查表
7.1 核心机制一句话
suspend函数 = 带Continuation参数的函数;每个挂起点切一个case(label);挂起时return COROUTINE_SUSPENDED保存状态并弹出栈;恢复时通过resumeWith重新进入函数,用label跳到对应case继续执行。
7.2 关键概念速查
| 概念 | 说明 |
|---|---|
suspend fun |
编译后多一个 Continuation 参数,返回 Object |
COROUTINE_SUSPENDED |
特殊标记,表示真的挂起了 |
Continuation.resumeWith |
恢复入口,重新进入状态机 |
label |
状态机状态,决定从哪个 case 继续 |
| 局部变量提升 | 跨挂起点使用的变量提升为 Continuation 字段 |
runBlocking |
阻塞当前线程跑事件循环,桥接阻塞/协程世界 |
withContext(IO) |
切换调度器,代码在 IO 线程池执行 |
7.3 状态机规则
- N 个挂起点 → N+1 个状态(label 0...N)→ 代码切成 N+1 段
- 每个挂起点前一个小
switch - 普通代码不增加
case,只塞进相邻段 - 跨挂起点变量提升为字段
7.4 线程模型对比
| 场景 | 执行线程 | 恢复后线程 |
|---|---|---|
runBlocking |
main | main(事件循环) |
withContext(Dispatchers.IO) |
IO 线程池 | 可能不同 IO 线程 |
Dispatchers.Main |
main | main |
Dispatchers.Default |
计算线程池 | 可能不同线程 |
7.5 三个"反直觉"但重要的点
- 状态机代码是同步的,异步性来自"挂起返回 + resumeWith 重新进入"这个循环。
runBlocking下全程 main 线程,只是执行时机被切成多段,不是多线程。Dispatchers.IO下同一状态机的多次进入可能在不同线程,由线程池调度决定。
7.6 常用并发写法
kotlin
// 并发执行多个独立任务
suspend fun loadAll() = coroutineScope {
val a = async { fetchA() }
val b = async { fetchB() }
"${a.await()} + ${b.await()}"
}
// 简化:awaitAll
suspend fun loadAllSimple() = coroutineScope {
listOf(async { fetchA() }, async { fetchB() }).awaitAll()
}
// 异常独立处理
suspend fun loadAllSafe() = coroutineScope {
val a = async { runCatching { fetchA() }.getOrDefault("失败") }
val b = async { runCatching { fetchB() }.getOrDefault("失败") }
"${a.await()} + ${b.await()}"
}
7.7 验证字节码
bash
kotlinc -Xno-optimize Example.kt
javap -c -p ExampleKt.class
会看到:
loadAll方法签名带Continuation- 方法体是
tableswitch(对应when(label)) if_acmpne之类指令判COROUTINE_SUSPENDED
附录:完整调用链示意
Kotlin 源码
│
│ 编译(CPS + 状态机)
▼
Java 状态机(switch + label + Continuation)
│
│ 运行
▼
runBlocking 创建事件循环
│
├─ 第一次进入(label 0)→ 遇到挂起点 → return SUSPENDED
│ │
│ 线程进入事件循环 park
│ │
├─ 定时器/IO 完成 → enqueue(resumeWith)
│ │
├─ 事件循环取出 → 第二次进入(label 1)→ 又挂起 → return SUSPENDED
│ │
├─ 再次完成 → enqueue → 第三次进入(label 2)→ 真正 return
│ │
└─ 结果沿 resumeWith 链上冒泡 → loop.complete → runBlocking 返回