Kotlin 协程源码解析(六)delay() 是如何挂起协程的?
在上一篇文章中,我们一直在研究 Continuation。
我们已经知道,Kotlin 编译器会把 suspend 函数转换成状态机,而 Continuation 保存了状态机继续执行所需要的信息。
当协程恢复时,大致会经历:
resumeWith()
↓
invokeSuspend()
↓
状态机继续执行
但是这里还留下了一个非常关键的问题:
一个已经挂起的协程,到底是谁在未来某个时刻调用了它的
resume()?
例如:
launch(Dispatchers.Main) {
println("A")
delay(1000)
println("B")
}
我们知道 delay(1000) 会让协程挂起。
但是:
delay()到底做了什么?- 它为什么不会阻塞线程?
- 挂起以后,Continuation 去哪里了?
- 未来又由谁负责恢复它?
这一篇我们先只解决前两个问题:
delay()是如何让当前协程挂起的?
至于 Android 是如何实现 Dispatchers.Main,以及 delay 到期以后到底是谁调用 resume(),我们留到后面的文章继续追踪。
一、delay() 并不是让线程等待
先看最简单的代码:
launch(Dispatchers.Main) {
println("A")
delay(1000)
println("B")
}
第一次接触协程时,很容易产生这样的直觉:
Main Thread
↓
delay(1000)
↓
等待 1000ms
↓
继续执行
如果真是这样,那么 delay() 和:
Thread.sleep(1000);
就没有本质区别了。
但实际上,delay() 不会阻塞当前线程。
真正发生的事情更接近:
协程
↓
保存继续执行所需要的信息
↓
安排未来恢复
↓
当前这次执行结束
线程则可以继续执行其他任务。
所以:
协程挂起,不等于线程等待。
协程只是暂时停止执行,而承载它的线程可以被释放出来执行其他工作。
二、从 delay() 源码开始
我们先直接看 Kotlin 协程中的 delay():
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return
return suspendCancellableCoroutine { cont ->
cont.context.delay
.scheduleResumeAfterDelay(timeMillis, cont)
}
}
真正值得关注的是:
suspendCancellableCoroutine { cont ->
cont.context.delay
.scheduleResumeAfterDelay(timeMillis, cont)
}
这里出现了两个非常重要的东西:
cont
和:
cont.context.delay
其中 cont 是当前协程对应的:
CancellableContinuation<Unit>
也就是说:
delay()拿到了当前协程的 Continuation。
然后它又从:
cont.context
中找到:
delay
并调用:
scheduleResumeAfterDelay(timeMillis, cont)
这个方法的名字其实已经把职责表达得非常清楚:
安排在指定的时间之后恢复这个 Continuation。
所以 delay() 自己并没有:
sleep()
也没有:
创建线程等待
它做的是:
当前 Continuation
↓
交给 Delay
↓
安排未来恢复
三、Delay 到底是什么?
Delay 可以理解成协程运行时提供的一种能力。
它的核心接口可以简化为:
interface Delay {
fun scheduleResumeAfterDelay(
timeMillis: Long,
continuation: CancellableContinuation<Unit>
)
}
这个接口的职责非常单纯:
在指定的时间之后恢复 Continuation。
它并不关心具体怎么实现。
例如它不需要知道:
Android Handler 怎么工作
也不需要知道:
Java Timer 怎么工作
更不需要知道:
线程池怎么实现
所以可以把它理解成一个抽象:
delay()
↓
“我需要等待一段时间”
↓
Delay
↓
“好,我负责安排未来的恢复”
这是一种非常典型的职责分离。
delay() 只负责表达:
我要延迟。
而具体的 Delay 实现负责:
怎么延迟,以及什么时候恢复。
四、suspendCancellableCoroutine 在这里做了什么?
现在再看:
suspendCancellableCoroutine { cont ->
cont.context.delay
.scheduleResumeAfterDelay(timeMillis, cont)
}
这里实际上完成了两个重要动作。
1. 获取当前协程的 Continuation
也就是:
当前协程
↓
CancellableContinuation
这个 Continuation 代表:
将来恢复时,从这里继续执行。
例如:
println("A")
delay(1000)
println("B")
编译器生成的状态机已经知道:
当前执行到 delay
↓
下一次恢复
↓
继续执行 println("B")
所以 Continuation 可以理解成:
状态机的一份“继续执行说明书”。
2. 把未来恢复的责任交出去
cont.context.delay
.scheduleResumeAfterDelay(timeMillis, cont)
意思就是:
Continuation
↓
交给 Delay
↓
Delay 负责安排未来恢复
于是 delay() 本身就没有必要继续等待了。
五、什么时候真正返回 COROUTINE_SUSPENDED?
这一步可以和我们上一篇研究过的 BaseContinuationImpl.resumeWith() 联系起来。
我们之前看到过这样的代码:
val outcome = invokeSuspend(param)
if (outcome === COROUTINE_SUSPENDED) {
return
}
现在把 delay() 放进去:
invokeSuspend()
↓
delay()
↓
suspendCancellableCoroutine
↓
安排未来恢复
↓
COROUTINE_SUSPENDED
最终当前状态机得到:
COROUTINE_SUSPENDED
于是:
if (outcome === COROUTINE_SUSPENDED) {
return
}
直接返回。
这里有一个非常重要的理解:
这个
return并不意味着协程结束了。
它只意味着:
当前这一次状态机执行结束了。
因为现在没有继续执行的条件,所以状态机暂时停下来。
而 Continuation 仍然存在。
状态机也仍然存在。
只是在等待未来某个时刻再次:
resume()
六、为什么线程没有被阻塞?
现在重新看:
delay(1000)
执行过程:
Main Thread
↓
执行 delay()
↓
获取 Continuation
↓
告诉 Delay:
“1000ms 后恢复这个 Continuation”
↓
返回 COROUTINE_SUSPENDED
↓
当前协程执行结束
注意最后一步:
当前协程执行结束
并不是:
线程等待 1000ms
所以 Main Thread 可以继续做自己的事情。
例如:
Main Thread
│
├── 执行协程 A
│ ↓
│ delay()
│ ↓
│ 挂起
│
├── 处理 UI 事件
│
├── 执行其他 Runnable
│
├── 执行协程 B
│
└── ...
这就是协程能够在等待期间释放线程的原因。
七、COROUTINE_SUSPENDED 到底意味着什么?
到这里,我们可以重新理解:
COROUTINE_SUSPENDED
它并不是:
“线程进入等待状态。”
也不是:
“协程创建了一个新的线程。”
它表达的是:
当前状态机暂时没有结果可以继续向下执行,调用者不应该再继续驱动这个 Continuation。
例如:
invokeSuspend()
↓
执行到 delay()
↓
注册未来恢复
↓
返回 COROUTINE_SUSPENDED
于是:
当前 resumeWith()
↓
结束
未来某个时刻,外部事件发生:
某个事件
↓
continuation.resume(...)
↓
再次进入状态机
↓
invokeSuspend()
↓
从之前保存的状态继续执行
因此,COROUTINE_SUSPENDED 实际上是连接:
这一次执行
和:
未来下一次执行
之间的边界。
八、delay() 为什么需要 Continuation?
如果我们把 delay() 想象成一个普通的同步函数:
fun delay(time: Long) {
Thread.sleep(time)
}
它不需要 Continuation,因为线程会停在那里。
但协程不能这么做。
协程必须在挂起之前保存:
以后应该从哪里继续?
例如:
launch {
println("A")
delay(1000)
println("B")
}
挂起的时候,我们不能只保存:
“我要等待 1 秒”
还必须保存:
“1 秒之后我要继续执行 println("B")”
而这个信息正是状态机和 Continuation 保存的。
所以整个过程可以理解成:
协程执行
│
▼
println("A")
│
▼
delay()
│
┌─────────┴─────────┐
│ │
▼ ▼
保存 Continuation 安排未来恢复
│ │
└─────────┬─────────┘
│
▼
COROUTINE_SUSPENDED
│
▼
当前执行结束
九、这里其实有两个完全不同的问题
到这里,我们可以把“挂起”拆成两个问题。
问题一:以后怎么继续?
答案是:
Continuation + 状态机
它保存:
当前状态
局部变量
completion
下一次应该执行的位置
所以:
Continuation 解决“怎么继续”。
问题二:什么时候继续?
答案是:
Delay
它负责:
安排未来某个时间点
↓
恢复 Continuation
所以:
Delay 解决“什么时候继续”。
这两个概念非常重要。
我们不能把:
Continuation
和:
Delay
混成一件事情。
Continuation 是:
继续执行所需要的信息。
Delay 是:
安排继续执行的机制。
十、现在还有一个关键问题没有回答
到这里,我们已经把:
delay(1000)
的“挂起”部分追通了:
delay()
↓
suspendCancellableCoroutine
↓
获取当前 Continuation
↓
获取 CoroutineContext 中的 Delay
↓
scheduleResumeAfterDelay()
↓
COROUTINE_SUSPENDED
↓
当前协程停止执行
但是我们故意留下了最后一段没有展开:
scheduleResumeAfterDelay()
↓
???
↓
resume()
到底是谁实现了 Delay?
我们的代码明明是:
launch(Dispatchers.Main) {
delay(1000)
}
那么:
Dispatchers.Main
和:
Delay
到底是什么关系?
为什么在 Android 中,它最终可以和:
Handler
Looper
MessageQueue
连接起来?
这就是下一篇文章要解决的问题。
下一篇我们会继续沿着:
scheduleResumeAfterDelay()
↓
???
↓
resume()
这条线继续追踪。
我们会看到 Android 的 Dispatchers.Main 是如何通过 Handler + Looper 实现延迟恢复的,
同时也会看看 Android 平台提供的 Main Dispatcher 是如何被 kotlinx.coroutines 找到并创建出来的。
这一篇我们仍然不会深入 Dispatcher 的调度算法, 而是只关注一个问题:
“delay 挂起以后,1000ms 到底发生了什么?”