协程到底是什么

一个可以暂停再继续的函数,不是线程——从零手写一个状态机来理解协程的本质

Coroutine 状态机 线程 vs 协程 协作式调度 suspend

目录导航(点击跳转)

一、先忘掉线程:从一个朴素的想法开始

1 普通函数:必须一口气跑完

把你脑子里的「线程」「CPU 核心」「操作系统」先放一边,从一个最朴素的想法开始:

// 普通函数:必须一口气跑完
void greet() {
    print("你好");
    print("世界");
}
// 调用 greet(),它两行打完,结束。你不能让它停在中间。
2 幻想中的「能暂停的函数」

但有时候你希望一个函数能停在中间,下次再从停的地方继续:

// 幻想中的「能暂停的函数」
void greet() {
    print("你好");
    pause;               // ← 停在这,把控制权还回去
    print("世界");        // ← 下次调用从这继续
}

协程就是这个幻想中的东西——一个可以暂停、以后再继续的函数。

二、Professional 的叫法

1 Coroutine = Co + Routine
Co
协作
+
Routine
例程
=
Coroutine
协作式地互相让路的执行单元

关键在「协作」二字:不是操作系统拿刀逼着你让路,而是你自己主动说「我暂时没事了,别人来」

三、用最土的 Java 代码模拟协程

1 手写一个能暂停继续的「函数」

不靠任何库,纯手写:

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: 结束");
        }
    }
}
2 执行过程拆解
调用者 MyCoroutine
1 调用 resume() → 打印"步骤1" → state=1 → 返回
第一次暂停,state 记住位置
2 调用 resume() → 打印"步骤2" → state=2 → 返回
第二次暂停,从 state=1 继续
3 调用 resume() → 打印"步骤3" → 结束
同一个函数,调三次,每次从上次停的地方接着走。这就是协程。
3 状态迁移图
state=0
步骤1
→ resume() →
state=1
步骤2
→ resume() →
state=2
步骤3
→ resume() →
结束

state 变量就是协程的「记忆」——记住上次跑到哪了,下次从这继续

四、编译器替你干这个事:Kotlin suspend

1 手写 state + if 太痛苦了

上面那个 MyCoroutine只有 3 步已经够啰嗦了,真实业务可能有几十个挂起点,手写状态机简直是地狱。所以——编译器替你干这个事

你只管写成顺序代码:

suspend fun greet() {
    println("步骤1: 开始")
    delay(1000)              // 挂起点:暂停 1 秒,这期间协程"出去了"
    println("步骤2: 中间")
    delay(1000)              // 又一个挂起点
    println("步骤3: 结束")
}

编译器帮你生成一个含 state 的类delay()就是暂停点,自动把 state 推进到下一阶段。你写的顺序三行代码,编译器帮你切成三段,每段之间可以插入「暂停-恢复」逻辑。

2 你看到的 vs 编译器生成的
你写的代码
suspend fun greet() {
  println("步骤1")
  delay(1000)
  println("步骤2")
  delay(1000)
  println("步骤3")
}
编译器生成的状态机(伪代码)
class GreetContinuation {
  int label = 0;
  switch(label) {
    case 0: println("步骤1");
      label=1; delay(1000, this)
    case 1: println("步骤2");
      label=2; delay(1000, this)
    case 2: println("步骤3")
  }
}

五、协程 vs 线程:一张表看懂

1 全面对比
维度 线程(Thread) 协程(Coroutine)
管理者 OS 内核管理 Runtime(用户态)管理
调度方式 抢占式:OS 随时打断你,你没得选 协作式:你自己主动让路(到挂起点才让)
栈位置 虚拟地址空间,固定 1MB 堆上,按需增长,起始几百字节
切换成本 要掉进内核(系统调用 + 上下文切换) 纯用户态函数调用
OS 感知 每个线程都是内核里的一个实体 对 OS 来说协程根本不存在
创建数量 几千个就到头了(栈内存 1MB × N) 可以创建百万个(每个几百字节起)
阻塞代价 阻塞整个线程,连带线程上所有任务 只挂起当前协程,线程立刻去跑别的

六、抢占式 vs 协作式:最核心的区别

1 两种调度哲学
线程(抢占式)

OS 的时钟中断,啪!你没得选。

你跑着跑着突然被踢下去

不知道什么时候会被切走。

→ 任何一行代码都可能是你的最后一口气

协程(协作式)

只有到你写了 delay()/ await()时才让。

主动说「我暂时没事了,别人来」。

精确知道你的暂停点在哪里。

→ 挂起点之间的代码是原子的(不会被切走)

2 抢占式 vs 协作式:示意图
OS 时钟 线程 协程
抢占式(线程)
线程正在执行代码...
时钟中断!OS 强制切走线程,换上另一个
线程自己完全不知道什么时候被切——任何指令都可能是断点
协作式(协程)
协程执行中...
遇到 delay() / await(),协程主动说「我让路」
协程精确知道自己的暂停点——只有显式的挂起函数才会让路

七、核心思想总结

第一章核心要点

一句话

协程就是一个状态机,编译器把你写的顺序代码切成一段一段的,每段之间可以暂停,下次从暂停的地方接着走。