Continuation 的链式关系 —— 一个 suspend 函数如何把结果交还给调用者?

# Kotlin 协程源码解析(四):Continuation 的链式关系------一个 suspend 函数如何把结果交给调用者?

在上一篇文章中,我们已经知道了 suspend 函数为什么能够挂起。

编译器会把 suspend 函数转换成状态机,并通过 Continuation 保存暂停之后继续执行所需要的信息。

例如:

kotlin 复制代码
suspend fun loadData() {
    val user = getUser()
    println(user)
}

我们可以把它粗略理解成:

kotlin 复制代码
fun loadData(continuation: Continuation<...>)

于是一个新的问题出现了:

如果一个 suspend 函数调用了另一个 suspend 函数,那么这两个函数之间的 Continuation 到底是什么关系?

例如:

kotlin 复制代码
suspend fun loadData() {
    val user = getUser()
    println(user)
}

loadData() 有自己的 Continuation。

getUser() 也会接收到一个 Continuation。

那么:

  • getUser() 的 Continuation 是谁?
  • getUser() 完成以后结果交给谁?
  • loadData() 又是怎么继续执行的?
  • 这些 Continuation 是不是彼此独立的?

理解这些问题,我们才能真正理解协程是如何"从一个挂起点继续执行"的。


一、suspend 函数不是协程

首先需要澄清一个非常容易产生的误解。

看到:

kotlin 复制代码
suspend fun getUser()

我们很容易认为:

getUser() 是一个协程。

实际上并不是。

suspend 函数只是一个可以挂起的函数

真正创建协程的是:

kotlin 复制代码
launch {
    ...
}

或者:

kotlin 复制代码
async {
    ...
}

例如:

kotlin 复制代码
launch {

    loadData()

}

这里才创建了一个协程。

之后的执行关系更接近:

text 复制代码
launch 创建协程
        |
        v
    loadData()
        |
        v
     getUser()

loadData()getUser() 并没有分别创建两个独立的协程。

它们只是同一个协程执行过程中的不同函数。

这一点非常重要。

如果把每个 suspend 函数都理解成一个独立协程,后面理解 Continuation 时就很容易走偏。


二、Continuation 不只是"保存暂停位置"

我们之前已经知道:

Continuation 用来保存协程挂起之后继续执行所需要的信息。

这个理解是正确的,但还不够完整。

Continuation 还有一个非常重要的属性:

它知道当前执行完成以后应该通知谁。

这个"谁",就是 completion

Kotlin 协程内部的 BaseContinuationImpl 中有一个非常重要的字段:

kotlin 复制代码
private val completion: Continuation<Any?>

于是可以把一个 Continuation 粗略理解成:

text 复制代码
Continuation
    |
    +-- 当前状态
    |
    +-- 当前执行上下文
    |
    +-- completion
            |
            v
        上一级 Continuation

这意味着:

Continuation 之间不是孤立存在的,它们可以连接起来。


三、一个 suspend 调用会形成 Continuation 链

来看一个简单的例子:

kotlin 复制代码
suspend fun loadData() {

    val user = getUser()

    println(user)
}

假设:

kotlin 复制代码
suspend fun getUser(): User {
    return requestUser()
}

执行 loadData() 时,我们可以粗略理解为:

text 复制代码
loadData 状态机
        |
        v
loadData Continuation

loadData() 调用 getUser() 时,需要有一个 Continuation 来描述 getUser() 当前的执行状态。

于是形成:

text 复制代码
getUser Continuation
        |
        | completion
        v
loadData Continuation

注意这里的方向。

getUser 的 Continuation 并不是一个孤立的对象。

它知道:

"当我完成以后,我应该把结果交给 loadData 的 Continuation。"

这就是 completion 的意义。


四、为什么 getUser 完成以后,结果能够回到 loadData?

这是整个问题最核心的地方。

假设:

kotlin 复制代码
suspend fun loadData() {

    val user = getUser()

    println(user)
}

执行到:

kotlin 复制代码
val user = getUser()

时,loadData 暂时无法继续,因为 getUser() 还没有产生结果。

于是可以形成:

text 复制代码
loadData Continuation
        ^
        |
   completion
        |
        |
getUser Continuation

getUser 可能因为网络请求而挂起。

未来网络请求完成以后:

text 复制代码
getUser Continuation
        |
        v
   resumeWith(result)

于是 getUser 的状态机继续执行。

如果 getUser 最终执行完成,它就需要把自己的结果交给:

text 复制代码
completion

也就是:

text 复制代码
getUser Continuation
        |
        | completion
        v
loadData Continuation

于是整个过程可以表示成:

text 复制代码
getUser.resumeWith(result)
        |
        v
getUser 状态机继续执行
        |
        v
getUser 执行完成
        |
        v
completion.resumeWith(result)
        |
        v
loadData 状态机继续执行
        |
        v
println(user)

所以:

suspend 函数之间不是通过普通的 return 把结果一层层返回,而是通过 Continuation 的 completion 链,把恢复结果交给调用者。


五、Continuation 链其实很像被搬到堆上的调用栈

普通函数调用:

kotlin 复制代码
fun A() {
    B()
}

fun B() {
    C()
}

fun C() {
}

执行过程中,线程调用栈大概是:

text 复制代码
A()
└── B()
    └── C()

C() 执行完成以后,线程栈天然知道:

text 复制代码
C → B → A

因为调用者就在栈上。

但是协程不能依赖这种机制。

假设:

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

执行到 B() 时挂起。

这时候线程可能直接返回线程池。

原来的线程栈就没有了。

未来恢复时甚至可能已经换了一个线程。

因此协程不能依赖:

text 复制代码
线程调用栈

来保存:

"我是被谁调用的?"

它必须自己保存。

于是出现了:

text 复制代码
Continuation A
        ^
        |
        |
Continuation B
        ^
        |
        |
Continuation C

这可以理解成:

Continuation 在堆上构建了一条类似调用栈的关系。

区别在于:

普通函数依赖:

text 复制代码
线程栈

协程依赖:

text 复制代码
Continuation 对象之间的关系

这也是协程能够挂起以后释放线程的重要基础。


六、为什么这不是三个协程?

现在再回头看:

text 复制代码
Continuation A
Continuation B
Continuation C

很容易产生另一个误解:

既然有三个 Continuation,是不是有三个协程?

不是。

它们只是同一个协程中,不同 suspend 调用层级对应的状态机对象。

例如:

text 复制代码
一个协程
    |
    +-- A 状态机
    |
    +-- B 状态机
    |
    +-- C 状态机

它们通过 completion 建立关系:

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

所以:

Continuation 的数量和协程的数量并不是一回事。

一个协程执行过程中完全可能存在多个 Continuation。


七、结果是如何一级一级传回去的?

假设有:

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

suspend fun B() {
    C()
}

suspend fun C(): String {
    return "Hello"
}

可以抽象成:

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

C 完成:

text 复制代码
C.resumeWith("Hello")

然后:

text 复制代码
C 状态机执行完成
        |
        v
C.completion.resumeWith("Hello")
        |
        v
B 状态机继续

如果 B 也完成:

text 复制代码
B.completion.resumeWith("Hello")
        |
        v
A 状态机继续

于是:

text 复制代码
C
↓
B
↓
A

结果就这样沿着 Continuation 链向上传递。


八、这也解释了为什么状态机可以恢复到正确的位置

假设:

kotlin 复制代码
suspend fun A() {

    val result = B()

    println(result)
}

第一次执行:

text 复制代码
A 状态机
    |
    v
调用 B()
    |
    v
保存状态
    |
    v
挂起

编译器生成的状态机可以粗略理解成:

text 复制代码
label = 0

调用 B()

保存必要的局部变量
label = 1

挂起

未来恢复:

text 复制代码
label = 1

拿到 B 的结果

继续执行:

println(result)

所以恢复并不是:

CPU 回到了原来暂停的那条指令。

而是:

重新执行状态机,根据保存的状态决定下一步应该执行什么。

Continuation 负责保存"怎么继续"。

状态机负责真正执行"继续做什么"。


九、现在可以回答最开始的问题了

我们最开始的问题是:

为什么一个 suspend 函数创建了自己的 Continuation,却还能把结果返回给调用它的 suspend 函数?

答案是:

因为这些 Continuation 并不是互相独立的。

它们通过 completion 形成了一条链。

例如:

text 复制代码
getUser Continuation
        |
        | completion
        v
loadData Continuation

getUser 完成以后:

text 复制代码
getUser Continuation
        |
        v
completion.resumeWith(result)
        |
        v
loadData Continuation
        |
        v
loadData 状态机继续执行

所以真正发生的不是传统意义上的:

kotlin 复制代码
return user

而是:

text 复制代码
子状态机完成
    ↓
通知 completion
    ↓
上一级状态机恢复
    ↓
继续执行

这就是 Continuation 链。


十、但是还有一个问题没有解决

到这里,我们终于理解了:

Continuation 如何把结果交给调用者。

但是一个更加基础的问题仍然没有回答:

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

下一篇文章我们将深入源码探索这个问题。

相关推荐
hunterandroid1 小时前
[Android 从零到一] Kotlin Channel 与复杂并发场景:从生产者-消费者到结构化并发治理
android
FlightYe2 小时前
linux系统编程(八):阻塞与非阻塞IO对比
android·linux·运维·服务器·github·vim
恋猫de小郭2 小时前
Flutter iOS Deep Link 为什么会突然失效:一系列难以言喻的问题
android·前端·flutter
Kapaseker3 小时前
小白都看得懂的 Skill 教程 - 初识 Skill
android·kotlin·vibecoding
神奇小梵3 小时前
安全和开发的需要了解的知识————下载内容的安全,和下载内容如何运行
android·windows·安全·个人开发
2501_915909063 小时前
iOS 证书创建与管理 类型限制 p12 密码与云备份实操
android·ios·小程序·https·uni-app·iphone·webview
黄毛火烧雪下3 小时前
Service 推送 / Stub·Proxy 对调
android
zhangphil3 小时前
Android Coil 3 ImageRequest.Builder error() 和 fallback()异同
android·kotlin
我命由我123454 小时前
Jetpack Compose - @Preview 注解
android·java·java-ee·android studio·android jetpack·android-studio·android runtime