一个可以暂停再继续的函数,不是线程——从零手写一个状态机来理解协程的本质
把你脑子里的「线程」「CPU 核心」「操作系统」先放一边,从一个最朴素的想法开始:
// 普通函数:必须一口气跑完
void greet() {
print("你好");
print("世界");
}
// 调用 greet(),它两行打完,结束。你不能让它停在中间。
但有时候你希望一个函数能停在中间,下次再从停的地方继续:
// 幻想中的「能暂停的函数」
void greet() {
print("你好");
pause; // ← 停在这,把控制权还回去
print("世界"); // ← 下次调用从这继续
}
协程就是这个幻想中的东西——一个可以暂停、以后再继续的函数。
关键在「协作」二字:不是操作系统拿刀逼着你让路,而是你自己主动说「我暂时没事了,别人来」。
不靠任何库,纯手写:
class MyCoroutine {
int state = 0; // 记着跑到哪了
void resume() {
if (state == 0) {
System.out.println("步骤1: 开始");
state = 1;
return; // 停!返回调用者
}
if (state == 1) {
System.out.println("步骤2: 中间");
state = 2;
return; // 再停!
}
if (state == 2) {
System.out.println("步骤3: 结束");
}
}
}
state 变量就是协程的「记忆」——记住上次跑到哪了,下次从这继续
上面那个 MyCoroutine只有 3 步已经够啰嗦了,真实业务可能有几十个挂起点,手写状态机简直是地狱。所以——编译器替你干这个事。
你只管写成顺序代码:
suspend fun greet() {
println("步骤1: 开始")
delay(1000) // 挂起点:暂停 1 秒,这期间协程"出去了"
println("步骤2: 中间")
delay(1000) // 又一个挂起点
println("步骤3: 结束")
}
编译器帮你生成一个含 state 的类,delay()就是暂停点,自动把 state 推进到下一阶段。你写的顺序三行代码,编译器帮你切成三段,每段之间可以插入「暂停-恢复」逻辑。
| 维度 | 线程(Thread) | 协程(Coroutine) |
|---|---|---|
| 管理者 | OS 内核管理 | Runtime(用户态)管理 |
| 调度方式 | 抢占式:OS 随时打断你,你没得选 | 协作式:你自己主动让路(到挂起点才让) |
| 栈位置 | 虚拟地址空间,固定 1MB | 堆上,按需增长,起始几百字节 |
| 切换成本 | 要掉进内核(系统调用 + 上下文切换) | 纯用户态函数调用 |
| OS 感知 | 每个线程都是内核里的一个实体 | 对 OS 来说协程根本不存在 |
| 创建数量 | 几千个就到头了(栈内存 1MB × N) | 可以创建百万个(每个几百字节起) |
| 阻塞代价 | 阻塞整个线程,连带线程上所有任务 | 只挂起当前协程,线程立刻去跑别的 |
OS 的时钟中断,啪!你没得选。
你跑着跑着突然被踢下去。
你不知道什么时候会被切走。
→ 任何一行代码都可能是你的最后一口气
只有到你写了 delay()/ await()时才让。
你主动说「我暂时没事了,别人来」。
你精确知道你的暂停点在哪里。
→ 挂起点之间的代码是原子的(不会被切走)
协程就是一个状态机,编译器把你写的顺序代码切成一段一段的,每段之间可以暂停,下次从暂停的地方接着走。