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() 被调用以后,为什么协程又可能跑到另外一个线程?
那时,DispatchedContinuation、CoroutineDispatcher 和 CoroutineScheduler 才会真正登场。