Kotlin 协程源码解析(五):BaseContinuationImpl.resumeWith() —— Continuation 链是如何被展开的?

Kotlin 协程源码解析(五):BaseContinuationImpl.resumeWith() ------ Continuation 链是如何被展开的?

上一篇文章我们解决了一个非常关键的问题:

一个 suspend 函数为什么能够把结果交给调用它的 suspend 函数?

答案是:

suspend 函数之间通过 Continuation 建立了一条链。

例如:

text 复制代码
C Continuation
        |
        | completion
        v
B Continuation
        |
        | completion
        v
A Continuation

当 C 完成以后:

text 复制代码
C
↓
B
↓
A

结果沿着 completion 一层层向上传递。

那么问题来了:

Kotlin 协程运行时究竟是怎么遍历这条 Continuation 链的?

这一次,我们直接进入源码。


一、BaseContinuationImpl 是 Continuation 链的重要节点

Kotlin 协程运行时有一个非常重要的基类:

kotlin 复制代码
abstract class BaseContinuationImpl(
    private val completion: Continuation<Any?>
) : Continuation<Any?>

编译器生成的很多状态机最终都会继承它。

因此可以把它粗略理解成:

text 复制代码
编译器生成的状态机
        |
        v
BaseContinuationImpl
        |
        +-- completion

当一个 suspend 函数调用另一个 suspend 函数时,Continuation 之间就通过:

kotlin 复制代码
completion

连接起来。


二、先看完整的 resumeWith()

现在直接看核心源码:

kotlin 复制代码
public final override fun resumeWith(result: Result<Any?>) {
    // This loop unrolls recursion in current.resumeWith(param) to make saner and shorter stack traces on resume
    var current = this
    var param = result

    while (true) {
        probeCoroutineResumed(current)

        with(current) {
            val completion = completion!!
            val outcome: Result<Any?> =
                try {
                    val outcome = invokeSuspend(param)
                    if (outcome === COROUTINE_SUSPENDED) return
                    Result.success(outcome)
                } catch (exception: Throwable) {
                    Result.failure(exception)
                }

            releaseIntercepted()

            if (completion is BaseContinuationImpl) {
                // unrolling recursion via loop
                current = completion
                param = outcome
            } else {
                // top-level completion reached -- invoke and return
                completion.resumeWith(outcome)
                return
            }
        }
    }
}

第一次看这段代码,最容易产生几个问题:

  • 为什么需要 while (true)
  • 为什么不是直接 completion.resumeWith(outcome)
  • current 为什么会不断变化?
  • param 为什么也会不断变化?
  • COROUTINE_SUSPENDED 为什么会直接 return
  • completion is BaseContinuationImpl 又是什么意思?

我们逐个拆开。


三、current 和 param 是什么?

一开始:

kotlin 复制代码
var current = this
var param = result

其实非常直观。

假设当前恢复的是:

text 复制代码
C Continuation

那么:

text 复制代码
current = C

传给 C 的恢复结果:

text 复制代码
result

则:

text 复制代码
param = result

于是:

kotlin 复制代码
invokeSuspend(param)

就是:

用当前恢复结果,让当前状态机继续执行。


四、invokeSuspend() 才是真正执行状态机的地方

resumeWith() 本身并不知道:

kotlin 复制代码
suspend fun A() {
    ...
}

里面具体应该执行什么。

真正知道的是编译器生成的状态机。

所以:

kotlin 复制代码
val outcome = invokeSuspend(param)

才是真正进入状态机的地方。

可以理解成:

text 复制代码
resumeWith()
    |
    v
invokeSuspend()
    |
    v
编译器生成的状态机

例如:

kotlin 复制代码
suspend fun loadData() {

    val user = getUser()

    println(user)
}

状态机大致会保存:

text 复制代码
label = 0

第一次执行:

text 复制代码
label = 0
    |
    v
调用 getUser()
    |
    v
保存状态
    |
    v
挂起

恢复以后:

text 复制代码
label = 1
    |
    v
获得 getUser 的结果
    |
    v
println(user)

因此:

resumeWith() 负责把恢复结果交给状态机,而 invokeSuspend() 负责让状态机继续执行。


五、COROUTINE_SUSPENDED 会让这次恢复立即结束

源码:

kotlin 复制代码
val outcome = invokeSuspend(param)

if (outcome === COROUTINE_SUSPENDED) {
    return
}

这里非常关键。

invokeSuspend() 执行以后有两种主要情况。

第一种:

text 复制代码
状态机执行完成

那么它返回真正的结果。

例如:

text 复制代码
User

第二种:

text 复制代码
状态机又一次挂起

那么它返回:

kotlin 复制代码
COROUTINE_SUSPENDED

于是:

kotlin 复制代码
if (outcome === COROUTINE_SUSPENDED) {
    return
}

当前这次 resumeWith() 直接结束。

这意味着:

一次恢复并不保证协程一定执行到最终完成。

恢复只是:

给状态机一次继续执行的机会。

如果状态机在执行过程中又遇到了新的挂起点,那么这次恢复就结束了。


六、如果状态机执行完成,结果应该交给谁?

假设:

text 复制代码
C Continuation
        |
        | completion
        v
B Continuation

C 执行完成以后:

kotlin 复制代码
val outcome = ...

得到:

text 复制代码
C 的结果

下一步自然应该是:

kotlin 复制代码
completion.resumeWith(outcome)

也就是:

text 复制代码
C 完成
    |
    v
B.resumeWith(C的结果)

看起来我们直接调用就可以了。

但是 Kotlin 并没有这么做。

而是先判断:

kotlin 复制代码
if (completion is BaseContinuationImpl)

为什么?


七、为什么 completion 是 BaseContinuationImpl 时不直接调用?

因为:

text 复制代码
completion

可能也是一个:

text 复制代码
BaseContinuationImpl

如果是,那么它很可能也是当前 Continuation 链中的一个状态机节点。

例如:

text 复制代码
C
|
completion
v
B
|
completion
v
A

如果 C 执行完成以后直接:

kotlin 复制代码
B.resumeWith(result)

那么 B 又可能:

kotlin 复制代码
A.resumeWith(result)

最终形成:

text 复制代码
C.resumeWith()
    ↓
B.resumeWith()
    ↓
A.resumeWith()

这就是普通的递归调用。


八、源码没有使用递归,而是把递归展开成循环

这正是:

kotlin 复制代码
while (true)

存在的原因。

源码:

kotlin 复制代码
if (completion is BaseContinuationImpl) {
    current = completion
    param = outcome
} else {
    completion.resumeWith(outcome)
    return
}

注意这里并没有:

kotlin 复制代码
completion.resumeWith(outcome)

而是:

kotlin 复制代码
current = completion
param = outcome

然后重新进入:

kotlin 复制代码
while (true)

于是:

text 复制代码
原本:

C.resumeWith()
    ↓
B.resumeWith()
    ↓
A.resumeWith()

被转换成:

text 复制代码
current = C
param = C的结果

while:

    执行 C

    current = B
    param = C的结果

    执行 B

    current = A
    param = B的结果

    执行 A

所以这里的 while 实际上是在:

迭代遍历 Continuation 链。


九、为什么要这么做?

源码自己的注释已经给出了答案:

kotlin 复制代码
// This loop unrolls recursion in current.resumeWith(param)
// to make saner and shorter stack traces on resume

也就是说:

这个循环把 resumeWith() 中的递归调用展开,从而让恢复过程产生更加合理、更加短的调用栈。

如果 Continuation 链很长:

text 复制代码
A
|
B
|
C
|
D
|
E
|
F
...

使用递归:

text 复制代码
resumeWith
    resumeWith
        resumeWith
            resumeWith
                ...

真实线程调用栈也会越来越深。

使用循环:

text 复制代码
while {
    invokeSuspend()
    current = completion
}

这些 Continuation 的恢复过程就在同一个循环里完成。

因此:

Continuation 链可以很长,但线程调用栈不需要随着 Continuation 链一起增长。

这不仅仅是为了避免栈溢出,也让协程恢复时的调试调用栈更加简洁。


十、为什么最后还要调用 completion.resumeWith()?

源码最后:

kotlin 复制代码
else {
    completion.resumeWith(outcome)
    return
}

这里说明:

text 复制代码
completion

已经不是:

kotlin 复制代码
BaseContinuationImpl

了。

也就是说:

当前已经到达这条状态机链的顶部。

继续向上已经没有编译器生成的状态机需要循环处理了。

因此直接:

kotlin 复制代码
completion.resumeWith(outcome)

把结果交给最终的 Continuation。

于是整个过程结束。

可以把它理解成:

text 复制代码
Continuation 链:

C
|
B
|
A
|
Top-level completion

恢复:

text 复制代码
current = C

↓
C 执行

current = B

↓
B 执行

current = A

↓
A 执行

↓
Top-level completion.resumeWith()

↓
结束

十一、于是 resumeWith() 的真正职责就非常清晰了

把源码压缩一下:

kotlin 复制代码
override fun resumeWith(result: Result<Any?>) {

    var current = this
    var param = result

    while (true) {

        val completion = current.completion

        val outcome = current.invokeSuspend(param)

        if (outcome === COROUTINE_SUSPENDED) {
            return
        }

        if (completion is BaseContinuationImpl) {

            current = completion
            param = outcome

        } else {

            completion.resumeWith(outcome)
            return
        }
    }
}

可以把它理解成:

text 复制代码
收到恢复结果
    |
    v
执行当前状态机
    |
    +------ 又挂起 ------> 返回
    |
    +------ 执行完成
               |
               v
          找到 completion
               |
               +------ 是状态机 ------> 继续循环
               |
               +------ 不是状态机 ----> 最终 resumeWith()

十二、一个很重要的认识:resumeWith() 不是"让协程运行起来"

看到这里,我们还需要特别纠正一个容易产生的误解。

我们经常说:

resumeWith() 恢复了协程。

这句话从使用层面没有问题,但从源码层面来看还不够精确。

BaseContinuationImpl.resumeWith() 做的事情其实是:

把一个结果交给状态机,然后驱动 Continuation 链继续执行。

它并不负责决定:

text 复制代码
在哪个线程执行?

也不负责决定:

text 复制代码
什么时候调用 resume?

甚至它也不知道:

text 复制代码
这个结果来自网络?
来自 delay?
来自 Binder?
来自文件 IO?

它只处理:

text 复制代码
结果来了
    |
    v
让状态机继续

至于:

结果什么时候来?

这是另一个问题。


十三、我们终于走到了下一个问题

到这里,我们已经把 Continuation 链和 resumeWith() 的关系真正串起来了:

text 复制代码
Continuation
    |
    +-- 状态机
    |
    +-- completion
            |
            v
        上一级 Continuation

恢复:

text 复制代码
resumeWith(result)
        |
        v
invokeSuspend()
        |
        +------ COROUTINE_SUSPENDED
        |          |
        |          v
        |       再次挂起
        |
        +------ 执行完成
                   |
                   v
              completion
                   |
                   v
            上一级状态机

但是现在一个更加关键的问题出现了:

到底是谁调用了 resume()

例如:

kotlin 复制代码
delay(1000)

协程挂起以后:

text 复制代码
Continuation 被保存
        |
        v
COROUTINE_SUSPENDED

1000 毫秒以后,究竟是谁找到了这个 Continuation?

又是谁调用:

kotlin 复制代码
continuation.resume(...)

再比如 Retrofit:

kotlin 复制代码
@GET("user")
suspend fun getUser(): User

网络请求完成以后:

OkHttp 的 Callback 和 Continuation 之间到底发生了什么?

这些问题目前还没有答案。

下一篇,我们暂时不讨论 Dispatcher,也不讨论 CoroutineScheduler。

我们先只追一件事情:

一个已经挂起的协程,到底是谁在未来某个时刻调用了它的 resume()

我们会从最简单的:

kotlin 复制代码
delay(1000)

开始,再进入 Retrofit 的:

text 复制代码
SuspendCancelableCoroutine

看看一个真正的异步回调,是如何最终变成:

kotlin 复制代码
continuation.resume(value)

的。

等这条链真正追通以后,我们再讨论:

resume() 被调用以后,为什么协程又可能跑到另外一个线程?

那时,DispatchedContinuationCoroutineDispatcherCoroutineScheduler 才会真正登场。

相关推荐
存在morning2 小时前
【PySpark 学习笔记 三】DataFrame API 入门
android·java·数据库
YSoup2 小时前
2026 安卓面试助手APP(安卓八股、题库)
android·面试·职场和发展
xcyxiner2 小时前
flutter 运行到模拟器上
android·前端·flutter
Android-Flutter2 小时前
android Glide 源码流程分析
android·kotlin
Android-Flutter3 小时前
android Glide 使用详解
android·kotlin
恋猫de小郭3 小时前
Dart 3.13 的到底改了什么?为什么很重要?有什么坑?
android·前端·flutter
HZZD_HZZD4 小时前
3T-UEM 数据建模实战:三维立方体 + LAM/ELA 双归因怎么落地?
android
Carson带你学Android4 小时前
腾讯正式上线AI应用生成平台:一句话生成 App,还能直接上架商店
android·ai编程
小陈的进阶之路4 小时前
Claude Code辅助测试:API测试与pytest自动化
android·开发语言·kotlin