# 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 链的?
下一篇文章我们将深入源码探索这个问题。