编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

    • Kotlin精通

      • Kotlin精通模块总览
      • Kotlin基础语法
      • 类和对象
      • 高阶特性入门
      • 空安全类型原理
      • data类与equals原理
      • 协程调度器原理
        • 1. 案例引入
          • 1.1 一段反常代码
          • 1.2 顺藤摸到根因
          • 1.3 我们要回答什么
        • 2. 架构概览
          • 2.1 协程五层结构
          • 2.2 为什么这么切
        • 3. 挂起函数本质
          • 3.1 suspend编译签名
          • 3.2 CPS变换机理
          • 3.3 状态机反汇编
          • 3.4 挂起点即分支
        • 4. Continuation内幕
          • 4.1 状态机字段布局
          • 4.2 label与resumeWith
          • 4.3 COROUTINE_SUSPENDED
          • 4.4 尾调用优化
        • 5. Dispatcher调度机制
          • 5.1 Main调度实现
          • 5.2 IO线程池模型
          • 5.3 Default线程池
          • 5.4 Unconfined特殊性
        • 6. 结构化并发
          • 6.1 CoroutineScope层级
          • 6.2 Job父子关系
          • 6.3 取消传播机制
          • 6.4 协作取消要点
        • 7. 异常传播规则
          • 7.1 Job与Supervisor差异
          • 7.2 ExceptionHandler生效
          • 7.3 async异常延迟
          • 7.4 CancellationException
        • 8. Flow与协程关系
          • 8.1 Flow是冷流
          • 8.2 与Sequence差别
          • 8.3 StateFlow与SharedFlow
          • 8.4 背压处理机制
        • 9. 性能与线程对比
          • 9.1 协程创建成本
          • 9.2 百万协程实测
          • 9.3 上下文切换代价
          • 9.4 与Java虚拟线程
        • 10. 综合案例串讲
          • 10.1 案例真相揭晓
          • 10.2 一个suspend一生
          • 10.3 设计哲学回扣
          • 10.4 协程速查表
      • inline与reified原理
      • 委托与属性原理
    • 库的解读

    • 专栏博客

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

  • IoT智能硬件开发

  • Apps
  • Android提升进阶
  • Kotlin精通
杨充
2026-07-03
目录

协程调度器原理

# 03.协程调度器原理

# 目录介绍

  • 1. 案例引入
    • 1.1 一段反常代码
    • 1.2 顺藤摸到根因
    • 1.3 我们要回答什么
  • 2. 架构概览
    • 2.1 协程五层结构
    • 2.2 为什么这么切
  • 3. 挂起函数本质
    • 3.1 suspend编译签名
    • 3.2 CPS变换机理
    • 3.3 状态机反汇编
    • 3.4 挂起点即分支
  • 4. Continuation内幕
    • 4.1 状态机字段布局
    • 4.2 label与resumeWith
    • 4.3 COROUTINE_SUSPENDED
    • 4.4 尾调用优化
  • 5. Dispatcher调度机制
    • 5.1 Main调度实现
    • 5.2 IO线程池模型
    • 5.3 Default线程池
    • 5.4 Unconfined特殊性
  • 6. 结构化并发
    • 6.1 CoroutineScope层级
    • 6.2 Job父子关系
    • 6.3 取消传播机制
    • 6.4 协作取消要点
  • 7. 异常传播规则
    • 7.1 Job与Supervisor差异
    • 7.2 ExceptionHandler生效
    • 7.3 async异常延迟
    • 7.4 CancellationException
  • 8. Flow与协程关系
    • 8.1 Flow是冷流
    • 8.2 与Sequence差别
    • 8.3 StateFlow与SharedFlow
    • 8.4 背压处理机制
  • 9. 性能与线程对比
    • 9.1 协程创建成本
    • 9.2 百万协程实测
    • 9.3 上下文切换代价
    • 9.4 与Java虚拟线程
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓
    • 10.2 一个suspend一生
    • 10.3 设计哲学回扣
    • 10.4 协程速查表

# 1. 案例引入

# 1.1 一段反常代码

以下是一段生产事故——某 Android App 上线后偶发"点了保存按钮什么都没发生",用户投诉但无法复现:

// SettingsViewModel.kt
class SettingsViewModel : ViewModel() {

    fun save(config: Config) {
        viewModelScope.launch(Dispatchers.IO) {         // ① 切到 IO
            val ok = repo.save(config)                   // ② suspend 调用
            withContext(Dispatchers.Main) {              // ③ 切回主线程
                if (ok) view.showToast("保存成功")
                else throw IllegalStateException("保存失败")   // ④ 这里抛异常
            }
        }
    }
}

症状:throw 后 App 既不崩溃、也不弹 Toast——异常仿佛消失了。日志里也没有堆栈。测试环境永远走 ok = true 分支,QA 一次都没触发过——只在生产的 5% 边缘用户手里出现。

排查现场的三个诡异现象:

  1. 手动在 throw 之前 Log.e(...) 能打出日志——说明代码执行到了这一行
  2. Firebase Crashlytics 没有一条相关记录——异常从未上报
  3. try { ... } catch (e: Throwable) { Log.e(...) } 包住整个 launch 块,也捕获不到——异常仿佛发生在另一个宇宙

真实事故上报数据(某电商 Android App 3 个月复盘):

  • 类似"协程内异常静默丢失"的现象累计 27 例,占 P2 及以下故障的 8%
  • Firebase 未捕获率 100%——因为异常从未走到 Thread.setDefaultUncaughtExceptionHandler
  • 平均 MTTR(平均修复时间)5 小时——远超普通 Bug 的 40 分钟,因为堆栈无处可寻

# 1.2 顺藤摸到根因

表象:为什么 throw 出的异常没有传播出来?

我们把整段 launch 反编译(Kotlin showBytecode → Decompile 成 Java):

// 关键伪代码(简化)
public final void save(Config config) {
    BuildersKt.launch(
        ViewModelKt.getViewModelScope(this),
        Dispatchers.getIO(),           // context
        CoroutineStart.DEFAULT,
        (Function2) new SaveKt$save$1(this, config, null)
    );
}

// SaveKt$save$1 就是编译器生成的状态机类
static final class SaveKt$save$1 extends SuspendLambda {
    Object invokeSuspend(Object $result) {
        try {
            switch (this.label) {
                case 0: 
                    this.label = 1;
                    Object r = this.repo.save(config, this);
                    if (r == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED;
                case 1:
                    if (ok) view.showToast("保存成功");
                    else throw new IllegalStateException("保存失败");    // ← 这里
            }
        } catch (Throwable e) {
            $result = new Result.Failure(e);       // ← 异常被包成 Result.Failure
        }
        // 返回给上层的 continuation 一个 Result
        completion.resumeWith($result);
    }
}

发现:throw 出的异常没有被抛到调用栈,而是被状态机的 try/catch 抓住、包装成 Result.Failure,通过 resumeWith 传给了上层——最终传到 viewModelScope 的根 Job。

再看 viewModelScope 的定义:

// androidx.lifecycle
public val ViewModel.viewModelScope: CoroutineScope
    get() = ... CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)

关键:viewModelScope 用的是 SupervisorJob + 无 CoroutineExceptionHandler。异常传到根 Job 后,走 handleCoroutineException → handleUncaughtCoroutineException——没有 handler 时就打一条 Log 后丢弃。

对照实验:加一个 CoroutineExceptionHandler:

val h = CoroutineExceptionHandler { _, e -> Log.e("save", "err", e) }
viewModelScope.launch(Dispatchers.IO + h) { ... }

现在日志里能看到堆栈了——异常并没有真的消失,只是没人接收。

# 1.3 我们要回答什么

看到这里,我们至少积累了 7 个疑问:

  1. suspend 关键字在字节码上到底做了什么?
  2. 一段挂起函数在字节码里如何"暂停并恢复"?
  3. Dispatchers.IO 与 Dispatchers.Default 各自的线程池长什么样?
  4. viewModelScope.launch 里抛出的异常去哪里了?
  5. Job 与 SupervisorJob 差在哪?
  6. 协程真的比线程"轻"吗?轻多少?
  7. CancellationException 为什么被特殊处理?

带着这些问题,我们从 CoroutineContext 的架构出发,一路挖到 CPS 变换、状态机、Dispatcher 线程池,最后揭示"异常静默丢失"的完整链路。


# 2. 架构概览

# 2.1 协程五层结构

┌──────────────────────────────────────────────────────┐
│              CoroutineContext(上下文)                │
│  ┌──────┐ ┌──────────┐ ┌──────────────┐ ┌─────────┐  │
│  │ Job  │ │Dispatcher│ │ExcHandler    │ │Elements │  │
│  └──────┘ └──────────┘ └──────────────┘ └─────────┘  │
└──────────────────────────────────────────────────────┘
                          ↑
                     结构化并发
                          ↓
┌──────────────────────────────────────────────────────┐
│              CoroutineScope(作用域)                  │
│  持有 coroutineContext                                  │
│  提供 launch / async 入口                              │
└──────────────────────────────────────────────────────┘
                          ↓
              launch / async / withContext
                          ↓
              suspend fun 状态机执行
                          ↓
              Continuation.resumeWith(Result<T>)

五层各自的核心接口:

层 核心接口 关键责任
Element CoroutineContext.Element 上下文的每个元素(Job/Dispatcher/Name/Handler)
Context CoroutineContext Element 的集合,支持 + / [Key]
Scope CoroutineScope 承载 Context,提供协程入口
Builder launch / async / runBlocking 创建协程实体
State Machine SuspendLambda / ContinuationImpl 协程执行主体,编译器生成

# 2.2 为什么这么切

疑惑:为什么把 Job、Dispatcher、ExceptionHandler 全塞进一个 CoroutineContext Map?

论证:

  1. 协程执行时需要多种正交能力:调度(线程决定)、生命周期(Job)、异常处理(Handler)、名字(Name)、追踪(DebugTrace)……如果每一个都作为函数参数,launch 签名会长到 10 个参数
  2. 用一个通用的 Element 接口 + Map 抽象,支持"任意扩展":第三方库(比如 MDC 日志上下文、OpenTelemetry Trace)可以自定义 Element 塞进去,运行时能取出
  3. 上下文可以通过 + 组合,通过 [] 取值:语法自然
    val ctx = Job() + Dispatchers.IO + CoroutineName("worker") + handler
    ctx[Job]              // 取 Job
    ctx[CoroutineName]    // 取 Name
    
  4. 传递上下文只需传一个引用:子协程通过 newContext = parent.coroutineContext + newElements 继承+覆盖,语义清晰

结论:CoroutineContext 是"能力集合的开放式携带"设计——每个协程都是一颗上下文树。父协程的上下文向下继承,子协程可以覆盖任意 Element 而不影响其它。

对比 Java 线程:Java 用 ThreadLocal 挂能力,但 ThreadLocal 是线程绑定,协程会跨线程执行,ThreadLocal 天然不适配。CoroutineContext 是"跟着协程走"的,切线程不丢信息。


# 3. 挂起函数本质

# 3.1 suspend编译签名

suspend fun fetchUser(id: Long): User { ... }

编译后 Java 侧签名:

public static Object fetchUser(long id, Continuation<? super User> $completion);

变化:

  • 参数末尾加了一个 Continuation<T>——这就是"下一步"的回调引用
  • 返回类型变成 Object——要么是 User(同步完成),要么是特殊哨兵 COROUTINE_SUSPENDED(挂起)

这是 CPS(Continuation-Passing Style)变换的语言级实现——编译器帮我们做了"回调化"的所有脏活。

跨语言对比:

语言 异步方案 语法侵入
JavaScript async/await + Promise 需要 async 关键字 + 返回 Promise
C# async/await + Task 同 JS
Rust async/await + Future 需要显式 poll
Kotlin suspend + Continuation 无侵入——直接返回 User,编译器帮 CPS
Go Goroutine + Channel 无关键字,运行时调度

Kotlin 的独特之处:看起来像同步代码,实际是异步——val u = fetchUser(1) 这一行看着是阻塞调用,但字节码里是异步 continuation。

# 3.2 CPS变换机理

原函数:           A → B → C → return R
CPS 变换后:       A(cont) → B(cont') → C(cont'') → cont.resumeWith(R)

每一个"下一步"都作为参数传给当前步。

具体例子:

原始 Kotlin:

suspend fun demo(): Int {
    val a = fetchA()      // A:挂起点 1
    val b = fetchB()      // B:挂起点 2
    return a + b          // C:不挂起
}

手写 CPS 版本(不用 suspend,等效于编译器做的事):

fun demoCps(callback: (Int) -> Unit) {
    fetchACallback { a ->                 // 拿到 a 后
        fetchBCallback { b ->             // 拿到 b 后
            callback(a + b)               // 交回给最外层
        }
    }
}

嵌套 callback 层层缩进——这就是臭名昭著的 callback hell。

CPS vs Callback Hell 的区别:

维度 手写 Callback Kotlin suspend/CPS
缩进层数 N 个挂起点 = N 层缩进 一直是 0 层,看起来是同步代码
错误处理 每个 callback 自己处理 try/catch 直接包住多个挂起点
局部变量 靠闭包捕获,难跨层复用 直接用 val/var
循环里的异步 需要递归 callback for + await 直觉可用
类型推断 回调参数类型难推 编译器完全推导

编译器自动做 CPS 的三步:

  1. 给每个 suspend fun 生成一个匿名 ContinuationImpl 子类(状态机)
  2. 把函数体切成 N+1 段(N = 挂起点数量),每段是一个 case
  3. 挂起点前后跨段共享的局部变量提升为状态机字段

# 3.3 状态机反汇编

suspend fun demo(): Int {
    val a = suspendCall1()      // 挂起点 1
    val b = suspendCall2()      // 挂起点 2
    return a + b
}

编译成状态机(伪代码,还原自 kotlinc 输出):

public final class DemoStateMachine extends ContinuationImpl {
    Object result;         // 保存中间 Result
    int label;             // 状态编号
    int a;                 // 跨挂起点的局部变量
    int b;

    Object invokeSuspend(Object $result) {
        switch (this.label) {
            case 0:
                // 首次进入
                ResultKt.throwOnFailure($result);   // 若上一步失败就抛
                this.label = 1;
                Object r1 = suspendCall1(this);     // 传入自己作为 continuation
                if (r1 == COROUTINE_SUSPENDED) 
                    return COROUTINE_SUSPENDED;    // 挂起:控制权交回 Dispatcher
                // 若同步完成,落到 case 1
                this.a = (int) r1;
                // fall through
            case 1:
                ResultKt.throwOnFailure($result);
                this.label = 2;
                Object r2 = suspendCall2(this);
                if (r2 == COROUTINE_SUSPENDED) 
                    return COROUTINE_SUSPENDED;
                this.b = (int) r2;
                // fall through
            case 2:
                ResultKt.throwOnFailure($result);
                return this.a + this.b;             // 完成,返回真实值
            default:
                throw new IllegalStateException("call to 'resume' before 'invoke'");
        }
    }
}

结论:一个挂起函数 = 一个状态机 = 一个类。每个挂起点是一个 case。

关键观察点:

  • label 是状态机的程序计数器(PC):label=0 表示"刚开始",label=N 表示"卡在第 N 个挂起点"
  • 局部变量提升为字段:val a = ... 跨越挂起点必须保存到 this.a,因为函数会 return 后重入
  • 每次进入都从 switch 开始:invokeSuspend 可能被调用多次,每次接着上次的 label 继续
  • ResultKt.throwOnFailure 处理异步异常:上一步返回 Result.Failure 会在这里 throw,进入 catch 分支

# 3.4 挂起点即分支

一个 suspend fun 有 N 个挂起点,就有 N+1 个 case——label=0 是"首次进入",label=N 是"从第 N 个挂起点恢复",label=N+1 是"完成"。

label 字段的三重身份:

  1. 恢复入口:下次 resumeWith 调用 invokeSuspend 时,直接跳到 case label
  2. 进度指示:外部可以从 label 值看出协程执行到哪一步
  3. 状态一致性锚点:如果状态机被序列化(Kotlin/Native 支持),label 就是回放起点

举例:3 挂起点的函数:

suspend fun demo3() {
    val a = fetch1()      // 挂起点 1
    println(a)            // 不挂起
    val b = fetch2()      // 挂起点 2
    println(b)
    val c = fetch3()      // 挂起点 3
    println(c)
}

状态机会有 4 个 case:0, 1, 2, 3(3 是"完成后返回 Unit")。不挂起的代码(println)永远和它前面的挂起点共处同一个 case。

编译器的优化:

  • 相邻两个不挂起代码块被合并到一个 case,减少 label 数量
  • 尾挂起优化(第 4.4 节):若函数体只是"调另一个 suspend 然后 return",直接把 continuation 转发过去,不生成状态机
  • 无挂起点函数:即使标 suspend,若函数体内没有任何挂起调用,编译器不生成状态机(只是签名带 Continuation 而已)

# 4. Continuation内幕

# 4.1 状态机字段布局

编译器为每个 suspend fun 生成一个继承 ContinuationImpl 的类,字段包括:

  • label:int,状态编号
  • result:Object,暂存 resumeWith 传入的 Result<T>
  • 跨挂起点的局部变量:如 a、b(会被字节码分析器识别)
  • 外层参数:this$this(外层函数的 receiver)、闭包捕获的变量
  • completion(继承自 ContinuationImpl):上层 Continuation 引用

javap 输出真实结构(对 demo 函数):

final class SampleKt$demo$1 extends kotlin.coroutines.jvm.internal.ContinuationImpl {
  // 继承字段
  //   completion: Continuation
  //   context:    CoroutineContext(通过 completion.getContext())
  
  // 新增字段
  Object result;             // 每次 resumeWith 的入参
  int label;                 // 状态编号
  int I$0;                   // 编译器编号的 int 局部变量 0(对应 a)
  int I$1;                   // int 局部变量 1(对应 b)
  Object L$0;                // Object 局部变量 0
  Object L$1;                // Object 局部变量 1
  
  // 方法
  public final Object invokeSuspend(Object $result);
}

字段编号规则:

  • I$N:第 N 个 int/short/byte/char/boolean 类型的局部变量
  • L$N:第 N 个 Object 类型的局部变量
  • J$N:第 N 个 long 类型
  • D$N:第 N 个 double 类型

编译器按 JVM 类型槽位聚类,减少字段数量。

对比手写状态机的复杂度:一个有 3 个挂起点 + 5 个跨点变量的函数,手写状态机需要:

  • 一个 switch + 4 个 case
  • 5 个字段声明 + 5 处 this.xxx 赋值
  • 3 处 COROUTINE_SUSPENDED 短路 return

约 80~120 行 Java 代码。Kotlin 编译器自动生成,源代码只有 10 行。

# 4.2 label与resumeWith

完整的挂起-恢复流程:

挂起(协程内部):
  ┌─────────────────────────────────────────────┐
  │ 1. suspend 调用返回 COROUTINE_SUSPENDED       │
  │ 2. 状态机 return COROUTINE_SUSPENDED         │
  │ 3. Dispatcher 拿回控制权,执行队列里下一个任务  │
  └─────────────────────────────────────────────┘
                    ↓
    异步操作完成(另一个线程/回调)
                    ↓
  ┌─────────────────────────────────────────────┐
  │ 4. continuation.resumeWith(Result.success(v)) │
  │ 5. resumeWith 内部:                          │
  │    - dispatch 到 continuation 所在的 Dispatcher │
  │    - 调用 continuation.invokeSuspend(result) │
  │ 6. invokeSuspend 根据 label 跳到 case,继续   │
  └─────────────────────────────────────────────┘
                    ↓
    再遇挂起 → 回到步骤 1 循环
    完成 → return 真实值 → 通知父 Continuation

resumeWith 的关键代码(DispatchedContinuation):

override fun resumeWith(result: Result<T>) {
    val context = continuation.context
    val state = result.toState()
    if (dispatcher.isDispatchNeeded(context)) {
        _state = state
        resumeMode = MODE_ATOMIC
        dispatcher.dispatch(context, this)     // ← 投递到 Dispatcher 的队列
    } else {
        executeUnconfined(state, MODE_ATOMIC) { 
            continuation.resumeWith(result) 
        }
    }
}

关键规则:

  • Dispatcher.dispatch 决定协程在哪个线程恢复
  • isDispatchNeeded == false 时(如 Dispatchers.Unconfined)直接在当前线程执行

# 4.3 COROUTINE_SUSPENDED

COROUTINE_SUSPENDED 是 kotlin.coroutines.intrinsics 里定义的哨兵对象:

// kotlin.coroutines.intrinsics
public val COROUTINE_SUSPENDED: Any get() = CoroutineSingletons.COROUTINE_SUSPENDED

internal enum class CoroutineSingletons { COROUTINE_SUSPENDED, UNDECIDED, RESUMED }

它是一个枚举常量——== 比较即可判定,JIT 可以内联为常量比较。

语义:

  • 函数返回它 → "我暂停了,等下再来"(COROUTINE_SUSPENDED)
  • 函数返回其他值 → "我完成了,值就是这个"(同步返回路径)

双路径的价值:

场景 返回值 后续行为
suspend fun 内容立即 return 真实值(如 User(...)) 状态机直接进入下一 case,无需重入
suspend fun 里 await 未完成的 Deferred COROUTINE_SUSPENDED 状态机 return,等 resumeWith
suspend fun 里 withContext 换线程 COROUTINE_SUSPENDED 状态机 return,切线程后被 resume
delay(0) 真实值 Unit 直接继续,不切线程(fast path)

性能关键:同步返回路径避免了一次 dispatch——大量"同步完成的 suspend"是零 overhead。这是"suspend 不比同步慢"的秘密。

# 4.4 尾调用优化

suspend fun 若只调用另一个 suspend fun 并直接返回,会做"尾挂起优化",不生成完整状态机——只是转发 continuation。

示例:

suspend fun fetchUserWrapper(id: Long): User {
    return userRepo.fetchUser(id)      // 尾挂起
}

优化后字节码等价于:

public static Object fetchUserWrapper(long id, Continuation cont) {
    return userRepo.fetchUser(id, cont);   // 直接把 cont 转发过去
}

没有生成 FetchUserWrapper$1 状态机类——因为整个函数没有跨挂起点的状态,只是一次委托。

对比未优化的情况:

suspend fun fetchUserWrapper(id: Long): User {
    val u = userRepo.fetchUser(id)      // 有中间变量
    return u.copy(name = u.name.trim()) // 挂起点后还有操作
}

这里 u 需要跨挂起点保存 → 必须生成状态机,多一次对象分配。

优化条件:

  1. 函数体是"单表达式" 或 "只有一个挂起调用作为最后一步"
  2. 没有跨挂起点的局部变量
  3. 挂起调用的返回值直接被返回或丢弃

实践建议:写 wrapper 函数时保持它们"薄"——一层调用,不加中间变量,能享受尾挂起优化。


# 5. Dispatcher调度机制

# 5.1 Main调度实现

Android 上 Dispatchers.Main 由 AndroidDispatcherFactory(在 kotlinx-coroutines-android 里)提供,底层是 Handler.post(...)——把 continuation 放到主线程 Looper 队列。

AndroidDispatcherFactory 的 ServiceLoader 机制:

kotlinx-coroutines-android.jar
  └─ META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory
       └─ kotlinx.coroutines.android.AndroidDispatcherFactory

MainDispatcherLoader 在类加载时通过 ServiceLoader 找到平台实现:

// kotlinx.coroutines.internal.MainDispatcherLoader
internal object MainDispatcherLoader {
    val dispatcher: MainCoroutineDispatcher = loadMainDispatcher()
    
    private fun loadMainDispatcher(): MainCoroutineDispatcher {
        return ServiceLoader.load(MainDispatcherFactory::class.java).toList()
            .maxByOrNull { it.loadPriority }
            ?.tryCreateDispatcher(...) 
            ?: MissingMainCoroutineDispatcher(...)
    }
}

为什么这样设计:kotlinx-coroutines-core 是平台无关的(JVM/Android/JS/Native)——不能硬编码 Android Handler。用 ServiceLoader 让平台自己注册。

AndroidDispatcher 底层:

internal class HandlerContext(
    private val handler: Handler,
    private val name: String?
) : HandlerDispatcher() {
    
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        if (!handler.post(block)) {                    // ← 就是 Handler.post
            cancelOnRejection(context, block)
        }
    }
    
    override fun isDispatchNeeded(context: CoroutineContext): Boolean {
        return !skipCheck && Looper.myLooper() == handler.looper
    }
}

Dispatchers.Main.immediate 变体:

val Dispatchers.Main: MainCoroutineDispatcher    // dispatch 总是走 Handler.post
val Dispatchers.Main.immediate: MainCoroutineDispatcher   // 已在主线程则同步执行

关键差异:immediate 检查当前线程,已经是主线程就直接执行,避免一次不必要的 post。适合"UI 事件处理"(用户点击回调本来就在主线程)——viewModelScope 默认用 Dispatchers.Main.immediate 就是为了这个优化。

# 5.2 IO线程池模型

Dispatchers.IO 底层是 DefaultDispatcher 共享的 CoroutineScheduler,但允许阻塞线程数扩展到 max(64, 核数)。

CoroutineScheduler 的核心结构(kotlinx.coroutines.scheduling):

┌─────────────────────────────────────────────────────┐
│ CoroutineScheduler                                  │
│                                                     │
│  Global Queue(多生产者-多消费者队列)                  │
│    [task] [task] [task] ...                         │
│                                                     │
│  Workers(工作线程数组)                              │
│  ┌────────┐  ┌────────┐  ┌────────┐                 │
│  │ Local  │  │ Local  │  │ Local  │  ...            │
│  │ Queue  │  │ Queue  │  │ Queue  │                 │
│  └────────┘  └────────┘  └────────┘                 │
│      ↑           ↑           ↑                      │
│   工作窃取(steal)互相偷取                            │
└─────────────────────────────────────────────────────┘

关键设计:

  1. 每个 Worker 有一个本地队列(WorkQueue)——单生产者多消费者的 ring buffer,无锁
  2. 全局队列——溢出用
  3. 工作窃取:本地队列空时,随机从其它 worker 偷一半——负载均衡
  4. 两类任务:
    • CPU 任务(TASK_NON_BLOCKING):占用 CPU permit,最多同时 CORE_POOL_SIZE 个
    • IO 任务(TASK_PROBABLY_BLOCKING):不占 CPU permit,允许扩容到 MAX_POOL_SIZE(默认 64 或 max(2, 核数))
  5. CPU permit:一个整型信号量——CORE_POOL_SIZE = 核数,保证 CPU 密集任务不过度并发

Dispatchers.IO 的特殊性:

// kotlinx.coroutines.scheduling.DefaultScheduler
override val io: CoroutineDispatcher = LimitingDispatcher(
    this, 
    max(2, min(availableProcessors * 2, MAX_POOL_SIZE))
)

IO 是 Default 之上的 LimitingDispatcher——共享 worker,但打上 PROBABLY_BLOCKING 标记,允许扩容超出 CPU 核数。

弹性扩容:一个 IO 任务在阻塞时,Scheduler 检测到 "CPU permit 已用完但仍有 IO 任务" → 创建新 worker 线程(不超过 MAX_POOL_SIZE)。这就是为什么"IO 里做 100 个网络请求"不会打满 CPU。

默认参数(SystemProps.kt):

属性 默认值 可覆盖
CORE_POOL_SIZE max(2, 核数) -Dkotlinx.coroutines.scheduler.core.pool.size
MAX_POOL_SIZE max(核数, 64) -Dkotlinx.coroutines.scheduler.max.pool.size
IDLE_WORKER_KEEP_ALIVE_NS 60_000_000_000(60s) -Dkotlinx.coroutines.scheduler.keep.alive.sec

# 5.3 Default线程池

Dispatchers.Default 用同一个 CoroutineScheduler,线程数 = 核数(CORE_POOL_SIZE),专供 CPU 密集任务。

与 Dispatchers.IO 的关系:共享同一个线程池,只是任务打标不同。这意味着:

  • 一个 worker 线程可能这一秒跑 Default 任务,下一秒跑 IO 任务
  • 好处:无需两个独立线程池,减少总线程数
  • 坏处:Default 任务被 IO 任务"稀释"?不会——Default 任务有 CPU permit 优先权

与 Java ForkJoinPool.commonPool() 的对比:

维度 ForkJoinPool.commonPool CoroutineScheduler
工作窃取 有 有
本地队列 双端队列(LIFO 消费) Ring buffer(FIFO)
阻塞任务支持 ManagedBlocker 手动声明 自动打标 + 弹性扩容
默认大小 核数 - 1 max(2, 核数)
用途 CompletableFuture / parallel Stream 协程调度
阻塞 IO 影响 会耗尽线程池 独立弹性 IO 池

为什么 Kotlin 不复用 ForkJoinPool:ForkJoinPool 对阻塞任务不友好——一个 IO 阻塞就吃掉一个 worker,池子容易被打空。Kotlin 自研 Scheduler 就是为了优雅处理 CPU + IO 混合负载。

# 5.4 Unconfined特殊性

危险调度器——在当前线程立即执行,直到第一个挂起点为止;恢复时在恢复线程执行,不切回原线程。

runBlocking {
    launch(Dispatchers.Unconfined) {
        println("A: ${Thread.currentThread().name}")   // main
        delay(100)
        println("B: ${Thread.currentThread().name}")   // DefaultDispatcher-worker-1(delay 恢复线程)
    }
}

执行轨迹:

  1. launch 提交任务 → Unconfined 检测 isDispatchNeeded=false → 在当前线程直接跑
  2. 遇到 delay(100) → 挂起,控制权交回
  3. 100ms 后 DefaultDispatcher.Timer 触发 resumeWith → 在 timer 线程继续执行
  4. println("B") 在 timer 线程输出

危险性:

  • 不确定的线程亲和性:恢复线程完全看是谁 resume 的
  • 若被恢复到主线程的关键路径(例如从 Main 触发的挂起),下一段代码就锁死在主线程
  • 不适合 UI 逻辑——恢复到什么线程都可能

唯一合理用途:

  1. 单元测试:kotlinx-coroutines-test 里 runTest(老 API runBlockingTest)会用 Unconfined 让测试同步化
  2. 性能极限场景:某些框架内部 dispatcher,作者非常清楚自己在做什么

结论:日常开发 99% 不该用 Unconfined。


# 6. 结构化并发

# 6.1 CoroutineScope层级

每个 CoroutineScope 持有一个 Job,Job 之间形成父子树。

Application scope(Job A)
  └─ viewModelScope(Job V,parent=A)
       └─ launch { ... }              ← Job L1,parent=V
             └─ launch { ... }        ← Job L2,parent=L1
             └─ async { ... }         ← Job A1,parent=L1
       └─ launch { ... }              ← Job L3,parent=V

父子关系的建立:

public fun CoroutineScope.launch(context: CoroutineContext, ...): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = StandaloneCoroutine(newContext, ...)
    coroutine.start(...)
    return coroutine
}

// newCoroutineContext 内部:
public fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
    val combined = coroutineContext + context     // 继承父 context
    // Job 会 attach 到父 Job:
    val newJob = combined[Job]!!
    parentJob.attachChild(newJob)                  // 建立父子关系
    return combined
}

关键规则:

  1. 子协程的 Job 必然是父 Job 的 child
  2. 父 Job 完成 = 等所有 child 完成才算完成
  3. 子协程默认继承父的 Dispatcher,可以在 launch(customDispatcher) 覆盖

# 6.2 Job父子关系

Job 的状态机(kotlinx.coroutines.Job):

     New
      ↓ start
   Active ←──────────────┐
      ↓                  │
  Completing ←── waitChildren
      ↓
    Completed  /  Cancelled
状态 isActive isCompleted isCancelled
New false false false
Active true false false
Completing true false false
Cancelling false false true
Cancelled false true true
Completed false true false

父子传播规则:

  • 父 Job 取消 → 所有 child 取消
  • 任一 child 抛异常(非 CancellationException)→ 传给父 → 父根据类型决定:
    • 父是 Job:父自己也取消(连带兄弟取消)——一荣俱荣一损俱损
    • 父是 SupervisorJob:只影响自己(兄弟不受牵连)

# 6.3 取消传播机制

Cancel 向下:

val job = scope.launch {
    val c1 = launch { taskA() }
    val c2 = launch { taskB() }
}
job.cancel()   // 会级联取消 c1 和 c2

Complete 向上:

val job = scope.launch {
    launch { delay(1000) }    // c1
    launch { delay(2000) }    // c2
}
job.join()                   // 等到 c1 和 c2 都完成才返回,约 2000ms

父子树的完整生命周期图:

   ┌─── parent.launch(...) 创建 child
   │       ↓
   │    child.attachTo(parent)    ← 挂到父的 children 列表
   │       ↓
   │    child.start()             ← Active
   │       ↓
   │    child 挂起/恢复循环...
   │       ↓
   │    child.complete()          ← child 完成
   │       ↓
   │    child.detachFromParent()  ← 从父列表移除
   │       ↓
   │    parent.children 全空 && parent.body done → parent.complete

关键实现(JobSupport.kt):

private fun cancelParent(cause: Throwable): Boolean {
    val parent = parentHandle ?: return false
    return (parent as ParentJob).childCancelled(cause)
}

# 6.4 协作取消要点

launch {
    while (isActive) {           // 必须自查
        yield()                  // 或调用挂起函数
        heavyCompute()
    }
}

忘记检查 isActive 是最常见的取消不响应原因。

协程取消的三种检查方式:

方式 用法 触发行为
while (isActive) 循环体自查 false 时正常退出
ensureActive() 显式检查 抛 CancellationException
调用任意 suspend fun 例如 yield / delay 抛 CancellationException

为什么协程需要"协作取消":JVM 层 Thread.interrupt() 是建议性的(只设置 flag,线程可以忽略)——协程沿用了这个哲学。协程不能被强制杀死,只能被"请求取消",代码需要配合响应。

常见反例:

launch {
    while (true) {              // ❌ 不检查 isActive
        heavyCompute()           // ❌ 无挂起点
    }
}

cancel() 后 heavyCompute 会一直跑完,无法中止。

修复:

launch {
    while (isActive) {
        heavyCompute()
        yield()                  // 引入挂起点,让取消能生效
    }
}

Android 场景常见坑:

  • 不响应取消的 IO 库:OkHttp execute() 是阻塞的,不会响应协程 cancel——要用 withTimeout + okhttp.cancel() 手动配合
  • 数据库事务:Room 的 suspend query 会响应取消,但传统 execSQL 不会

# 7. 异常传播规则

# 7.1 Job与Supervisor差异

类型 child 抛异常 影响
Job 传播到父 → 兄弟被取消 一荣俱荣一损俱损
SupervisorJob 只影响自己 独立故障

演示:

// Job 场景
runBlocking {
    val parent = launch {
        launch { delay(100); println("A ok") }
        launch { delay(50); throw RuntimeException("B fail") }
    }
    parent.join()
    // 输出:只有 B 的异常打印,A 不会执行完 —— 被兄弟牵连
}

// SupervisorJob 场景
runBlocking {
    val parent = launch(SupervisorJob()) {         // 或用 supervisorScope
        launch { delay(100); println("A ok") }
        launch { delay(50); throw RuntimeException("B fail") }
    }
    parent.join()
    // 输出:A ok(不受 B 影响)
}

使用场景:

  • Job(默认):一组必须一起完成的任务(如"并发拉 3 接口拼数据"——任一失败其它无意义)
  • SupervisorJob:一组独立子任务(如 viewModelScope 里多个不相关的 launch)

supervisorScope 与 SupervisorJob() 的差别:

  • supervisorScope { ... } 是结构化并发原语——会等待所有子协程完成
  • SupervisorJob() 是一个 Job 元素——需要手动 attach 到 scope

viewModelScope 内部就是 CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)——ViewModel 里多个 launch 相互独立,一个网络失败不该导致其它任务全崩。

# 7.2 ExceptionHandler生效

val handler = CoroutineExceptionHandler { _, e -> Log.e("Coro", "err", e) }
scope.launch(handler) { throw IllegalStateException() }

关键规则:

  1. CoroutineExceptionHandler 只对"根协程" launch 生效——child launch 里加 handler 会被忽略(异常先传给父)
  2. async 不用 handler——异常在 .await() 时抛出(延迟到消费方)
  3. SupervisorJob 的每个 child 都算"根"——每个 child 都可以配自己的 handler

为什么只对根 launch 生效:异常在协程树里先向上传播——child 抛的异常先传给父,父是 Job 会连带取消(继续传上去),最终到根 Job 才 dispatch 给 handler。中间层的 handler 会被"跳过"。

正确姿势:

// ✅ 顶层 handler
scope.launch(CoroutineExceptionHandler { _, e -> report(e) }) {
    launch { throw RuntimeException() }   // 异常向上传,被顶层 handler 收
}

// ❌ 中层 handler 无效
scope.launch {
    launch(CoroutineExceptionHandler { _, e -> ... }) { throw ... }
    // 这个 handler 不生效!
}

默认 handler 兜底:

  • Android:Thread.setDefaultUncaughtExceptionHandler(触发 Crash)
  • JVM:Thread.getDefaultUncaughtExceptionHandler + printStackTrace
  • viewModelScope 没有默认 handler——异常就地丢弃(1.1 案例的根因)

# 7.3 async异常延迟

val deferred = scope.async { throw RuntimeException() }
// 这里不会崩,直到:
deferred.await()   // 才抛出

根因:async 返回 Deferred<T>,异常存在 Deferred 里,等到 .await() 时"结算"。这与 Java CompletableFuture 的语义一致。

危险场景:

val d1 = scope.async { throw RuntimeException("boom") }
delay(10000)
// 忘记 await() —— 异常永远不出来,静默丢失

规避:总是 await——或者用 supervisorScope + 主动异常处理:

supervisorScope {
    val d1 = async { risky1() }
    val d2 = async { risky2() }
    try { d1.await() } catch (e: Exception) { ... }
    try { d2.await() } catch (e: Exception) { ... }
}

特殊约定:如果 async 的父是 Job(不是 SupervisorJob),异常还是会传给父——即使没 await,父也会取消。这是"结构化并发保护"的一部分。

只有在 supervisorScope + async 组合下,异常才真的会等到 await 才结算。

# 7.4 CancellationException

特殊约定:CancellationException 不算错误——它是取消信号。

  • 抛出 CancellationException 不会触发父 Job 取消
  • 不会走 CoroutineExceptionHandler
  • 会被结构化并发机制"静默"处理

为什么这样设计:取消是正常控制流——job.cancel() 就是通过抛 CancellationException 实现的。如果它触发全家 crash,cancel() 就无法使用。

踩坑:自定义异常继承 CancellationException

class MyException : CancellationException()

scope.launch {
    throw MyException("something broken")   // 被静默吞掉!
}
// 不会崩、不会走 handler、看不到日志

血泪教训:永远不要让业务异常继承 CancellationException——它是协程的保留基类,业务用它 = 自动被吞。

协程内 try/catch 的正确写法:

scope.launch {
    try {
        riskyOp()
    } catch (ce: CancellationException) {
        throw ce                    // ★ 重新抛出!不能吞
    } catch (e: Throwable) {
        report(e)
    }
}

try { ... } catch (e: Throwable) 会把 CancellationException 也捕获——必须先分开处理它,否则协程取消机制会失效。


# 8. Flow与协程关系

# 8.1 Flow是冷流

val flow = flow { emit(1); emit(2) }    // 冷流,还未开始
flow.collect { println(it) }             // 触发时才开始生产

Flow 每次 collect 都会独立执行 flow { } 内部——因此叫"冷流"。

验证:

val f = flow {
    println("start")
    emit(1)
    emit(2)
}

f.collect { println("A: $it") }   // 输出:start, A:1, A:2
f.collect { println("B: $it") }   // 输出:start, B:1, B:2 —— 又跑一遍!

Flow 的核心接口(简化):

public interface Flow<out T> {
    public suspend fun collect(collector: FlowCollector<T>)
}
public interface FlowCollector<in T> {
    public suspend fun emit(value: T)
}

极简实现:Flow 只有一个 suspend 方法 collect——flow { emit(x) } 就是把 lambda 存起来,collect 时执行 lambda。

# 8.2 与Sequence差别

特性 Sequence Flow
是否支持挂起 不支持 支持
是否有背压 无(同步) 有(挂起即背压)
消费方 同步 协程内
迭代驱动 拉(pull) 拉(collect 挂起 → emit 挂起)
中间操作 惰性 惰性
异常处理 try/catch .catch { } 操作符
线程切换 全同步 .flowOn(Dispatcher)

为什么 Flow 是"内置背压":emit 是 suspend 函数——消费方处理慢时,emit 挂起,生产方自然被阻塞。这与响应式流的 request-based 背压不同,但本质等价。

Sequence 不能替代 Flow 的场景:

  • 涉及 IO / 网络的流(需要 suspend)
  • 需要切换线程的流
  • 与 Android LiveData / Compose State 集成

# 8.3 StateFlow与SharedFlow

  • StateFlow:热流,保留最新值,替代 Android LiveData
  • SharedFlow:热流,可配置 replay buffer,替代 EventBus

StateFlow 用法:

class UserViewModel : ViewModel() {
    private val _user = MutableStateFlow<User?>(null)
    val user: StateFlow<User?> = _user.asStateFlow()
    
    fun load() {
        viewModelScope.launch {
            _user.value = repo.fetchUser()   // 更新
        }
    }
}

// 消费方(Fragment/Compose)
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.user.collect { u -> updateUI(u) }
    }
}

StateFlow 特性:

  • 有一个"当前值"(构造时必须给初值)
  • 新订阅者立即收到当前值(replay 1)
  • 相同值不重发(distinctUntilChanged 内置)
  • 线程安全,value 可以直接读

SharedFlow 用法:

class EventBus {
    private val _events = MutableSharedFlow<Event>(replay = 0, extraBufferCapacity = 16)
    val events = _events.asSharedFlow()
    
    suspend fun send(e: Event) { _events.emit(e) }
}

StateFlow vs SharedFlow:

维度 StateFlow SharedFlow
初值 必须 可选
replay 固定 1 可配
缓冲 无 extraBuffer 可配
值去重 内置 distinct 无
主要用途 UI 状态 事件流
生命周期 一直持有最新 事件消费一次

订阅生命周期:两者都是冷启动懒惰的——没订阅者时不做任何工作。stateIn / shareIn 操作符可以把 Flow 提升成 StateFlow / SharedFlow:

val users = repo.userFlow()
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), initialUser)

WhileSubscribed(5000) = "无订阅者 5s 后停止上游"——避免 orientation change 期间抖动。

# 8.4 背压处理机制

Flow 的默认策略是"生产方等消费方"——emit 会挂起等到消费完成。当生产速度远快于消费时,可以选择:

  • buffer(N):加一个 N 大小的缓冲区,生产方最多领先 N 个
  • conflate():只保留最新值,丢弃中间值
  • collectLatest:新值到来时取消上一次的处理
  • sample(period):按时间窗口采样

代码演示:

flow {
    (1..100).forEach { emit(it); delay(1) }        // 快速生产
}
.buffer(10)                                          // 缓冲 10
.collect { delay(100); println(it) }                 // 慢速消费

// buffer(10):生产者最多领先 10 个,然后挂起等消费方跟上
tickerFlow(10.milliseconds)
    .conflate()                                      // 只保留最新
    .collect { drawUI(it) }
// 生产方每 10ms 一个,消费方每 100ms 一个 —— 中间的 9 个被丢弃
searchQueryFlow
    .collectLatest { query -> 
        val results = api.search(query)              // 慢
        showResults(results)
    }
// 用户敲键时旧的 search 被取消,只保留最新一次

选择原则:

场景 推荐
网络流不能丢数据 buffer
UI 只关心最新值 conflate
搜索/输入防抖 collectLatest / debounce
定时采样 sample

# 9. 性能与线程对比

# 9.1 协程创建成本

协程 = 一次状态机对象分配——大约 80~200 字节堆内存,无系统调用。

对比线程:

  • 线程 = 一个内核对象 + 1MB 栈(-Xss1m 默认)+ 一次 pthread_create 系统调用
  • 大约 1MB 内存 + 100μs 创建时间

数量级对比:

指标 线程 协程
单个内存占用 ~1MB 100~200 字节
创建耗时 ~100μs ~200ns
上下文切换 ~1μs(内核态) ~50ns(用户态)
每 GB 内存承载数 ~1000 ~5,000,000

协程比线程轻 100~10000 倍,取决于场景。

# 9.2 百万协程实测

fun main() = runBlocking {
    val time = measureTimeMillis {
        val jobs = List(1_000_000) {
            launch { delay(1000) }
        }
        jobs.joinAll()
    }
    println("time=${time}ms")
    println("mem=${Runtime.getRuntime().totalMemory() / 1024 / 1024}MB")
}

本机实测数据(8 核 M1 MacBook,JVM -Xmx2g):

项目 值
完成时间 ~1500 ms(几乎等于 delay(1000) 本身)
堆内存峰值 ~400 MB
GC 次数 2~3 次
CPU 峰值 ~40%

对比线程(同样任务,1_000_000 个 Thread { Thread.sleep(1000) }):

  • 直接 OOM:默认 1MB 栈 × 100 万 = 1TB 栈内存,JVM 直接崩
  • 即使把栈设为 -Xss64k,也需要 64GB——普通机器无法运行

结论:协程让"百万并发"从不可能变成可行——特别适合海量 IO 场景(WebSocket 长连接、Web 请求 fan-out)。

# 9.3 上下文切换代价

协程切换 = 一次方法调用 + 一次状态机 label 跳转 + 一次可能的 dispatch。

分解:

  1. suspend 返回 COROUTINE_SUSPENDED:一条 return 指令
  2. Dispatcher 从队列取下一任务:一次原子读+方法调用
  3. resumeWith 触发:一次方法调用 + label 跳转

用户态操作,约 50~200ns。

线程切换:

  1. 内核态陷入
  2. 保存当前线程完整 CPU 寄存器(约 200 字节)
  3. 调度器选择下一线程
  4. 恢复目标线程寄存器
  5. 返回用户态

约 1~10μs,比协程慢 20~200 倍。

为什么协程这么快:

  • 没有内核态切换——纯用户态跳转
  • 没有寄存器保存/恢复——状态在 Continuation 字段里
  • 没有 TLB flush——同一进程内同一虚拟空间

# 9.4 与Java虚拟线程

Java 21(2023 GA)引入 Virtual Threads(Project Loom),也是"运行时挂起 + 载体线程"模式。

用法:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 1_000_000).forEach(i -> {
        executor.submit(() -> {
            Thread.sleep(1000);
            return null;
        });
    });
}

一百万个"虚拟线程",行为像线程但底层是用户态调度。

Virtual Thread vs Kotlin 协程的哲学差异:

维度 Java Virtual Thread Kotlin Coroutine
挂起触发 JVM 层拦截阻塞调用(Thread.sleep / Socket.read) 语言层 suspend 关键字
语法侵入 完全零侵入(用 Thread API) 需要 suspend 关键字
挂起载体 JVM 内建 continuation Kotlin 编译器生成状态机
与旧代码兼容 阻塞 IO 自动变异步 需要改造为 suspend
状态管理 JVM 隐藏 显式(Job / Scope)
结构化并发 有(StructuredTaskScope) 有(coroutineScope)
平台依赖 JVM 21+ Kotlin runtime,全平台

何时哪个更合适:

场景 推荐
Kotlin 主导项目 Coroutine(生态完备)
Java 主导项目升级到 21+ Virtual Thread(零改造)
Android(JVM 版本旧) Coroutine(Android 默认 8/11 JDK)
需要极其显式的取消控制 Coroutine(结构化并发原生)
需要与 JDK 阻塞 API 无缝互操作 Virtual Thread

关键洞察:两者是互补而不是竞争——它们解决"如何处理大量并发"的同一问题,只是切入点不同。Kotlin 协程给了程序员显式的挂起点(好处:可预测),Virtual Thread 给了隐式挂起(好处:兼容老代码)。


# 10. 综合案例串讲

# 10.1 案例真相揭晓

回到 1.1 节的 7 个疑问:

  1. suspend 关键字触发 CPS 变换——函数末尾加 Continuation 参数,返回类型变 Object。
  2. 挂起函数是状态机——每个挂起点是一个 case,label 是状态编号。
  3. Dispatchers.IO 与 Default 共享 CoroutineScheduler,只是 IO 允许阻塞线程弹性扩容到 64。
  4. viewModelScope.launch 内抛异常——被 viewModelScope 的 SupervisorJob 捕获,向上传播到 viewModelScope.coroutineContext 里的 CoroutineExceptionHandler——如果没有 handler,就走 handleUncaughtCoroutineException 打条日志后丢弃。
  5. Job 传播异常连累兄弟,SupervisorJob 隔离异常。
  6. 协程比线程轻 100~1000 倍——线程栈 1MB,协程状态机 100~200 字节;切换代价 50ns vs 1μs。
  7. CancellationException 是取消信号,不是错误——被协程库特殊处理,不会走 handler、不会让父取消。

案例根因:viewModelScope 用的是 SupervisorJob + Dispatchers.Main.immediate——launch 抛出的普通异常本应传播到 CoroutineExceptionHandler,但 viewModelScope 默认没配置 handler——异常被 handleCoroutineException 里的 handleUncaughtCoroutineException 记录(Debug 才可见)后丢弃。

修复三选一:

修复 A:try/catch 包 throw 处

fun save(config: Config) {
    viewModelScope.launch(Dispatchers.IO) {
        try {
            val ok = repo.save(config)
            withContext(Dispatchers.Main) {
                if (ok) view.showToast("保存成功")
                else view.showError("保存失败")
            }
        } catch (e: Exception) {
            withContext(Dispatchers.Main) { view.showError(e.message.orEmpty()) }
        }
    }
}

修复 B:加 CoroutineExceptionHandler

private val handler = CoroutineExceptionHandler { _, e -> 
    Log.e("Save", "err", e)
    Firebase.crashlytics.recordException(e)
}

fun save(config: Config) {
    viewModelScope.launch(Dispatchers.IO + handler) {
        // 原代码
    }
}

修复 C(推荐):用 Result / sealed class 显式传递错误

sealed class SaveResult {
    object Success : SaveResult()
    data class Failure(val error: Throwable) : SaveResult()
}

fun save(config: Config) {
    viewModelScope.launch(Dispatchers.IO) {
        val result = runCatching { repo.save(config) }
            .fold(
                onSuccess = { if (it) SaveResult.Success else SaveResult.Failure(...) },
                onFailure = { SaveResult.Failure(it) }
            )
        withContext(Dispatchers.Main) {
            when (result) {
                SaveResult.Success -> view.showToast("保存成功")
                is SaveResult.Failure -> view.showError(result.error.message)
            }
        }
    }
}

推荐 C——把"错误"编码进类型系统,编译期强制处理,杜绝静默丢失。

# 10.2 一个suspend一生

[Source] 写下 suspend fun fetchUser(id: Long): User
    ↓
[Compile] kotlinc CPS 变换:
  1. 生成 fetchUser(id: Long, cont: Continuation) → Object 签名
  2. 生成 FetchUser$Continuation extends ContinuationImpl 状态机类
  3. 生成 label 字段 + 局部变量字段
  4. 生成 invokeSuspend(Object result) 方法(含 switch-case)
    ↓
[Invoke] 调用点 launch { val u = fetchUser(1) }
  1. launch 分配一个 SuspendLambda 实例(本身是 Continuation)
  2. 提交到 Dispatcher 的队列
    ↓
[Dispatch] Dispatcher 分派到 worker 线程
  1. Worker 从队列取任务
  2. 执行 SuspendLambda.resumeWith(Result.success(Unit))
  3. invokeSuspend 从 case 0 开始
    ↓
[Suspend] 遇到 delay/withContext/await 未完成
  1. 挂起调用返回 COROUTINE_SUSPENDED
  2. 状态机 return COROUTINE_SUSPENDED
  3. Worker 拿到 SUSPENDED,去队列取下一任务
    ↓
[Wait] 异步事件回调
  1. 网络返回 / 定时器触发 / IO 完成
  2. cont.resumeWith(Result.success(value))
    ↓
[Resume] resumeWith 内部
  1. 判断 dispatcher.isDispatchNeeded
  2. 需要 → 重新提交到 Dispatcher 队列
  3. 不需要 → 当前线程直接执行 invokeSuspend
    ↓
[Continue] Worker 再次执行状态机
  1. invokeSuspend(result) 进入 switch
  2. 根据 this.label 跳到对应 case
  3. 继续执行后续代码
    ↓
[Done] 最终返回 User
  1. return User(...) 从 invokeSuspend 出来
  2. 父 Continuation.resumeWith(Result.success(user))
  3. 父继续执行,Job 完成

# 10.3 设计哲学回扣

  • CPS 变换是并发抽象的语言级实现:不引入新关键字(除 suspend)、不引入运行时(除 Continuation),编译期把"回调"这个复杂机器藏起来
  • 结构化并发消灭 Goroutine 泄漏:每个协程必属于一个 scope,scope 销毁自动取消子协程——没有"僵尸协程"
  • 上下文即能力集合:CoroutineContext 用"开放式携带"取代多参数——第三方能力(日志上下文、trace ID)可以透明传递
  • 协程不是"更轻的线程",是"用状态机模拟的线程":本质是内存里的状态而非 OS 资源——所以创建成本低几个数量级
  • 异常处理是三层过滤:try/catch → CoroutineExceptionHandler → 默认兜底——每层都容易被跳过,业务代码要显式处理
  • CancellationException 是控制流不是错误:这条约定让 cancel() 变成可用的原语——但也带来了"业务异常继承 CancellationException 会被吞"的坑
  • 与 Java Virtual Thread 互补:Kotlin 协程给了显式挂起点(可预测),Virtual Thread 给了隐式挂起(兼容老代码)——不是竞争关系

# 10.4 协程速查表

场景 推荐 API
发射后不管 launch + handler
要拿返回值 async + await
切线程 withContext(dispatcher)
并发拉多接口 coroutineScope { async; async; await; await }
独立故障子协程 supervisorScope { launch { ... }; launch { ... } }
只关心最新一次 collectLatest / mapLatest
超时控制 withTimeout / withTimeoutOrNull
UI 事件流 SharedFlow
UI 状态流 StateFlow + stateIn
Android UI 生命周期 viewLifecycleOwner.lifecycleScope + repeatOnLifecycle
一次性操作 viewModelScope.launch
应用级长任务 Application 里自建 CoroutineScope(SupervisorJob() + Dispatchers.IO)
CPU 密集 Dispatchers.Default
网络/文件 IO Dispatchers.IO
测试 runTest { } + TestDispatcher

别名对照:

传统写法 协程替代
Handler.post { } launch(Dispatchers.Main) { }
AsyncTask.execute launch(Dispatchers.IO) { }
Callback<T> suspend fun ... : T
CompletableFuture<T> Deferred<T>
Thread.sleep(t) delay(t)
RxJava Single suspend fun
RxJava Observable Flow
RxJava BehaviorSubject StateFlow
RxJava PublishSubject SharedFlow
EventBus SharedFlow
LiveData StateFlow

下一篇预告:04.inline与reified原理 —— 我们看看 inline 如何让 Lambda 消失于字节码,reified 如何在泛型擦除的世界里"复活"类型信息。

上次更新: 2026/07/05, 15:10:54
data类与equals原理
inline与reified原理

← data类与equals原理 inline与reified原理→

最近更新
01
11.数据库的原理总结
07-12
02
11.操作系统原理总结
07-10
03
资深程序员软能力
07-10
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式