Kotlin 协程原理详解 —— 以 delay 为例

Kotlin 协程原理详解 ------ 以 delay 为例

目录

  1. 核心结论
  2. [suspend 函数编译产物:状态机](#suspend 函数编译产物:状态机 "#suspend-%E5%87%BD%E6%95%B0%E7%BC%96%E8%AF%91%E4%BA%A7%E7%89%A9%E7%8A%B6%E6%80%81%E6%9C%BA")
  3. 挂起全流程
  4. 恢复全流程
  5. 关键源码逐层拆解
  6. 完整时序图
  7. 一图总结

核心结论

挂起 ≠ 阻塞线程。 挂起本质是:把"回调地址"(Continuation)存起来,然后一路 return,把线程还给线程池。 恢复本质是:定时器到期后,用存好的 Continuation 重新调用状态机,从上次的断点继续。


suspend 函数编译产物:状态机

Kotlin 源码

kotlin 复制代码
class Test {
    fun testDelay() {
        GlobalScope.launch {
            delay(111)                    // 挂起点 ①
            Log.d(TAG, "testDelay: 1111")
            delay(222)                    // 挂起点 ②
            Log.d(TAG, "testDelay: 222")
            self()                        // 挂起点 ③
        }
    }

    suspend fun self() {
        delay(333)                        // 挂起点 ④
        Log.d(TAG, "self: 333")
    }
}

编译后等价 Java(状态机骨架)

编译器将每个 suspend 调用点编译为一个 label 状态invokeSuspend 通过 switch(label) 实现"从断点继续"。

java 复制代码
// launch { } 的 lambda 体被编译成这个状态机
Object invokeSuspend(Object $result) {

    Object SUSPENDED = IntrinsicsKt.getCOROUTINE_SUSPENDED(); // 哨兵值

    switch (label) {

        case 0:                                   // 首次执行
            throwOnFailure($result);
            label = 1;                            // 保存断点
            Object r = delay(111L, this);         // 调用 suspend 函数
            if (r == SUSPENDED) return SUSPENDED; // ← 挂起点①,直接 return

        case 1:                                   // delay(111) 完成后从这里恢复
            throwOnFailure($result);
            Log.d(TAG, "testDelay: 1111");
            label = 2;
            r = delay(222L, this);
            if (r == SUSPENDED) return SUSPENDED; // ← 挂起点②

        case 2:                                   // delay(222) 完成后从这里恢复
            throwOnFailure($result);
            Log.d(TAG, "testDelay: 222");
            label = 3;
            r = self(this);
            if (r == SUSPENDED) return SUSPENDED; // ← 挂起点③

        case 3:                                   // self() 完成后从这里恢复
            throwOnFailure($result);
            return Unit.INSTANCE;                 // 协程正常结束
    }
}

关键设计:

  • label 在调用 suspend 函数之前更新,保证挂起后能从正确位置恢复
  • COROUTINE_SUSPENDED 是一个全局单例哨兵对象,用于区分"已挂起"和"同步完成"两种情况

挂起全流程

delay(111) 为例,从调用到线程释放,共经历 3 层 return

第一层:delay() 源码

kotlin 复制代码
// kotlinx/coroutines/Delay.kt
public suspend fun delay(timeMillis: Long) {
    if (timeMillis <= 0) return // 不需要等待,直接返回

    // suspendCancellableCoroutine 是最底层的挂起原语
    // 它会:1. 把当前 Continuation 包装成 CancellableContinuationImpl
    //       2. 执行 block(注册定时器)
    //       3. 返回 COROUTINE_SUSPENDED
    return suspendCancellableCoroutine { cont ->

        // 从协程上下文取出 Delay 实现(由 Dispatcher 提供)
        // Dispatchers.Default → DefaultDelay(ScheduledThreadPoolExecutor)
        // Dispatchers.Main    → HandlerContext(Handler.postDelayed)
        cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
        //                    ↑ 把 cont(回调地址)交给定时器保管
        //                      定时器到期后会调用 cont.resumeWith(Unit)
    }
    // 注意:先注册定时器,再返回 COROUTINE_SUSPENDED
    // 顺序不能反!必须先存好回调,再让出线程
}

第二层:invokeSuspend() 接到哨兵值后 return

java 复制代码
// invokeSuspend 内部,case 0
label = 1;
Object r = DelayKt.delay(111L, this); // delay 返回 COROUTINE_SUSPENDED
if (r == SUSPENDED) return SUSPENDED; // ← invokeSuspend 在这里 return
                                      //   把哨兵值往上传

第三层:BaseContinuationImpl.resumeWith() 接到哨兵值后 return

kotlin 复制代码
// kotlin/coroutines/jvm/internal/BaseContinuationImpl.kt
// 这是框架的"蹦床驱动器",while 循环驱动状态机
final override fun resumeWith(result: Result<Any?>) {
    var current = this
    var param = result

    while (true) {
        val outcome = try {
            val outcome = current.invokeSuspend(param) // 调用状态机
            if (outcome === COROUTINE_SUSPENDED)
                return  // ← resumeWith 在这里 return,线程彻底释放
            Result.success(outcome)
        } catch (e: Throwable) {
            Result.failure(e)
        }
        // 若 invokeSuspend 同步完成(未挂起),继续驱动父 Continuation
        val completion = current.completion!!
        current = completion as BaseContinuationImpl
        param = outcome
    }
}

三层 return 示意

kotlin 复制代码
delay()            return COROUTINE_SUSPENDED
                          │
                          ↓
invokeSuspend()    return COROUTINE_SUSPENDED
                          │
                          ↓
resumeWith()       return(退出 while 循环)
                          │
                          ↓
               线程回到线程池,空闲,可执行其他协程

恢复全流程

111ms 后,定时器触发,Continuation 被唤醒,经过以下链路重新执行状态机。

第一步:定时器到期,调用 resumeWith

kotlin 复制代码
// DefaultDelay 源码(Dispatchers.Default 使用)
override fun scheduleResumeAfterDelay(
    timeMillis: Long,
    continuation: CancellableContinuation<Unit>
) {
    val future = executor.schedule({           // ScheduledThreadPoolExecutor
        with(continuation) {
            resumeUndispatched(Unit)           // 111ms 后这里被执行
        }
    }, timeMillis, TimeUnit.MILLISECONDS)

    // 支持取消:协程取消时同时取消定时器
    continuation.invokeOnCancellation {
        future.cancel(false)
    }
}

第二步:CancellableContinuationImpl 派发到目标线程

kotlin 复制代码
// CancellableContinuationImpl.resumeWith(简化)
override fun resumeWith(result: Result<Unit>) {
    // 检查取消状态
    if (cancelled) {
        notifyParentOfCancellation()
        return
    }

    // 通过 Dispatcher 把恢复任务投递到协程绑定的线程
    // Dispatchers.Default → 扔进线程池队列
    // Dispatchers.Main    → handler.post(runnable)
    dispatcher.dispatch(context, DispatchedContinuation(this, result))
}

第三步:BaseContinuationImpl.resumeWith 再次调用 invokeSuspend

kotlin 复制代码
// 线程池线程(或主线程)拿到任务后执行
BaseContinuationImpl.resumeWith(Result.success(Unit))
    └─ invokeSuspend(Result.success(Unit))  // 再次进入状态机
           └─ switch(label = 1)            // 从上次断点继续
                  throwOnFailure($result)
                  Log.d(TAG, "testDelay: 1111")
                  label = 2
                  delay(222, this) ...     // 下一个挂起点

关键源码逐层拆解

suspendCancellableCoroutine ------ 挂起原语

kotlin 复制代码
// kotlin/coroutines/cancellation/CancellableContinuation.kt
public suspend inline fun <T> suspendCancellableCoroutine(
    crossinline block: (CancellableContinuation<T>) -> Unit
): T = suspendCoroutineUninterceptedOrReturn { uCont ->

    // 1. 用拦截器包装原始 Continuation(使其在正确线程上执行)
    val cancellable = CancellableContinuationImpl(uCont.intercepted(), ...)

    // 2. 执行 block(注册定时器、注册网络回调等)
    block(cancellable)

    // 3. 若已经完成(极少数同步场景),直接返回结果
    //    否则返回哨兵值,触发挂起
    cancellable.getResult() // 通常返回 COROUTINE_SUSPENDED
}

ContinuationImpl ------ 自定义 suspend 函数的 Continuation

每个自定义 suspend fun 都有一个专属的 ContinuationImpl 子类,负责:

  1. 保存局部变量(相当于栈帧快照)
  2. 保存 label(断点)
  3. 被唤醒时重入状态机
kotlin 复制代码
// self() 对应的 Continuation(编译器生成)
private inner class SelfContinuation(completion: Continuation<Unit>)
    : ContinuationImpl(completion) {

    var result: Any? = null  // 上一步的返回值
    var label: Int = 0       // 当前断点
    var self\$ref: Test? = null // 保存局部变量(L$0)

    // 被调度器唤醒时,框架调用此方法
    override fun invokeSuspend(result: Any?): Any? {
        this.result = result
        this.label = this.label or Int.MIN_VALUE // 设最高位,标记"这是恢复调用"
        return self(this)                        // 重入 self() 状态机
    }
}

BaseContinuationImpl.resumeWith ------ 蹦床驱动器

kotlin 复制代码
// 用 while 循环代替递归,避免栈溢出(Trampoline 模式)
final override fun resumeWith(result: Result<Any?>) {
    var current = this
    var param = result

    while (true) {
        val outcome = try {
            val outcome = current.invokeSuspend(param)
            if (outcome === COROUTINE_SUSPENDED) return // 挂起,退出
            Result.success(outcome)
        } catch (e: Throwable) {
            Result.failure(e)
        }

        // 当前层执行完,向上驱动父 Continuation(调用链)
        val completion = current.completion!!
        if (completion is BaseContinuationImpl) {
            // 父层也是状态机,继续循环(避免递归)
            current = completion
            param = outcome
        } else {
            // 到顶了(AbstractCoroutine),通知协程完成
            completion.resumeWith(outcome)
            return
        }
    }
}

完整时序图

sequenceDiagram autonumber participant U as 调用方线程 participant F as 协程框架
(BaseContinuationImpl) participant T as 定时器线程
(ScheduledExecutor) U->>F: GlobalScope.launch { ... } Note over F: 创建 StandaloneCoroutine
调度到线程池线程 A Note over F: ── 线程池线程 A ── F->>F: resumeWith(Unit) ← 首次启动 F->>F: invokeSuspend(Unit) [label = 0] F->>T: delay(111, this)
scheduleResumeAfterDelay(111ms, cont) Note over T: 保存 cont,注册定时器 T-->>F: return COROUTINE_SUSPENDED F-->>F: return COROUTINE_SUSPENDED Note over F: resumeWith 退出 while 循环
线程 A 释放 ✓ Note over T: ⏱ 111ms 后,定时器到期 T->>F: cont.resumeWith(success(Unit)) Note over F: dispatcher.dispatch(task)
投递到线程池线程 B Note over F: ── 线程池线程 B ── F->>F: invokeSuspend(success) [label = 1] Note over F: throwOnFailure()
Log.d("testDelay: 1111") F->>T: delay(222, this)
scheduleResumeAfterDelay(222ms, cont) T-->>F: return COROUTINE_SUSPENDED Note over F: 线程 B 释放 ✓ Note over T: ⏱ 222ms 后,定时器到期 T->>F: cont.resumeWith(success(Unit)) Note over F: 投递到线程池线程 C Note over F: ── 线程池线程 C ── F->>F: invokeSuspend(success) [label = 2] Note over F: Log.d("testDelay: 222")
进入 self() 状态机 [label = 0] F->>T: delay(333, selfCont)
scheduleResumeAfterDelay(333ms, selfCont) T-->>F: return COROUTINE_SUSPENDED Note over F: 线程 C 释放 ✓ Note over T: ⏱ 333ms 后,定时器到期 T->>F: selfCont.resumeWith(success(Unit)) Note over F: 投递到线程池线程 D Note over F: ── 线程池线程 D ── F->>F: self() 状态机 [label = 1] Note over F: Log.d("self: 333")
return Unit F->>F: while 循环向上驱动
launch body [label = 3]
return Unit F->>U: StandaloneCoroutine.resumeWith(success)
协程生命周期结束 ✓

一图总结

flowchart TD A(["`**invokeSuspend** label=0`"]) B["`delay(111, this)`"] C[/"`scheduleResumeAfterDelay 保存 cont,注册定时器`"/] D(["`return **COROUTINE_SUSPENDED**`"]) E(["`resumeWith 退出 while 🟢 线程释放`"]) F{{"⏱ 111ms 后"}} G["`定时器线程 cont.resumeWith(success)`"] H["`dispatcher.dispatch 投递到目标线程`"] I(["`**invokeSuspend** label=1 从断点继续执行`"]) A --> B B --> C C --> D D --> E E -.-> F F --> G G --> H H --> I I --> |"下一个 suspend 调用"| B style A fill:#4A90D9,color:#fff,stroke:none style I fill:#4A90D9,color:#fff,stroke:none style D fill:#E8A838,color:#fff,stroke:none style E fill:#27AE60,color:#fff,stroke:none style F fill:#8E44AD,color:#fff,stroke:none style C fill:#ECF0F1,stroke:#BDC3C7 style G fill:#ECF0F1,stroke:#BDC3C7 style H fill:#ECF0F1,stroke:#BDC3C7
  • 🔵 蓝色:状态机入口(invokeSuspend)
  • 🟠 橙色:哨兵值传递(挂起信号)
  • 🟢 绿色:线程释放点
  • 🟣 紫色:时间流逝 / 定时器到期

三个关键设计原则

原则 说明
先存后走 scheduleResumeAfterDelay 必须在 return COROUTINE_SUSPENDED 之前完成,否则回调地址丢失
哨兵传递 COROUTINE_SUSPENDED 是一个对象引用比较(===),逐层 return,不占用任何阻塞资源
蹦床模式 resumeWithwhile 循环代替递归驱动状态机,避免 self()delay() 调下一层时栈溢出
相关推荐
NAGNIP2 小时前
面试官:给我讲一下卷积吧!
算法·面试
NAGNIP3 小时前
一文搞懂卷积神经网络!
算法·面试
张元清3 小时前
使用 Hooks 构建无障碍 React 组件
前端·javascript·面试
studyForMokey3 小时前
【Android面试】窗口机制专题
android·面试·职场和发展
indexsunny4 小时前
互联网大厂Java面试:从Spring Boot到微服务的逐步挑战
java·数据库·spring boot·redis·微服务·面试·电商
郝学胜-神的一滴5 小时前
冷却时间下的任务调度最优解:从原理到实现
数据结构·c++·算法·面试
czlczl200209255 小时前
Dijkstra实现
面试
木斯佳6 小时前
前端八股文面经大全: 蓝色光标前端一面OC(2026-03-23)·面经深度解析
前端·面试·vue·校招·js·面经
野犬寒鸦7 小时前
从零起步学习AI大模型应用开发 || 第三章:智能体项目实战中的问题与解决方案及思路详解
java·服务器·数据库·人工智能·后端·面试