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

58 阅读3分钟

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

目录

  1. 核心结论
  2. suspend 函数编译产物:状态机
  3. 挂起全流程
  4. 恢复全流程
  5. 关键源码逐层拆解
  6. 完整时序图
  7. 一图总结

核心结论

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


suspend 函数编译产物:状态机

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) 实现"从断点继续"。

// 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() 源码

// 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

// 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/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 示意

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

恢复全流程

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

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

// 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 派发到目标线程

// 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

// 线程池线程(或主线程)拿到任务后执行
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/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. 被唤醒时重入状态机
// 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 —— 蹦床驱动器

// 用 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 协程框架<br/>(BaseContinuationImpl)
    participant T  as 定时器线程<br/>(ScheduledExecutor)

    U->>F: GlobalScope.launch { ... }
    Note over F: 创建 StandaloneCoroutine<br/>调度到线程池线程 A

    Note over F: ── 线程池线程 A ──
    F->>F: resumeWith(Unit) ← 首次启动
    F->>F: invokeSuspend(Unit)  [label = 0]

    F->>T: delay(111, this)<br/>scheduleResumeAfterDelay(111ms, cont)
    Note over T: 保存 cont,注册定时器
    T-->>F: return COROUTINE_SUSPENDED

    F-->>F: return COROUTINE_SUSPENDED
    Note over F: resumeWith 退出 while 循环<br/>线程 A 释放 ✓

    Note over T: ⏱ 111ms 后,定时器到期
    T->>F: cont.resumeWith(success(Unit))
    Note over F: dispatcher.dispatch(task)<br/>投递到线程池线程 B

    Note over F: ── 线程池线程 B ──
    F->>F: invokeSuspend(success)  [label = 1]
    Note over F: throwOnFailure()<br/>Log.d("testDelay: 1111")

    F->>T: delay(222, this)<br/>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")<br/>进入 self() 状态机 [label = 0]

    F->>T: delay(333, selfCont)<br/>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")<br/>return Unit

    F->>F: while 循环向上驱动<br/>launch body [label = 3]<br/>return Unit

    F->>U: StandaloneCoroutine.resumeWith(success)<br/>协程生命周期结束 ✓

一图总结

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() 调下一层时栈溢出