Kotlin 协程源码解析:协程是如何切换线程的?
之前我们已经研究过协程的挂起和恢复。
当一个协程挂起之后,某个事件发生,例如 delay() 到期、网络请求返回,最终都会调用 Continuation.resume(),让协程继续执行。
但这时候还有一个问题:
如果协程原本运行在线程 A,而恢复它的时候使用的是 Dispatchers.Default,它是怎么跑到线程 B 上继续执行的?
以前我们可能会简单地说:
Dispatcher 把协程切换到了线程池中的另一个线程。
但如果真正去看源码,会发现事情并不是某个地方简单地调用了一个"切换线程"的 API。
这次我们就沿着源码一路追下去,看看一个协程恢复请求,究竟是怎么从当前线程走到 CoroutineScheduler,再走到 Worker,最后重新执行协程代码的。
从 DispatchedContinuation 开始
之前研究 delay() 和 Retrofit 的时候,我们已经见过 DispatchedContinuation。
当协程恢复时,会进入类似这样的代码:
kotlin
internal inline fun resumeCancellableWith(result: Result<T>) {
val state = result.toState()
if (dispatcher.safeIsDispatchNeeded(context)) {
_state = state
resumeMode = MODE_CANCELLABLE
dispatcher.safeDispatch(context, this)
} else {
// ...
}
}
这里有两个非常重要的地方。
首先:
scss
dispatcher.safeIsDispatchNeeded(context)
会判断当前的恢复是否需要经过 Dispatcher。
如果需要,就执行:
kotlin
dispatcher.safeDispatch(context, this)
这里的 this 是谁?
就是当前的 DispatchedContinuation。
而 DispatchedContinuation 本身实现了 Runnable:
kotlin
internal class DispatchedContinuation<in T>(
internal val dispatcher: CoroutineDispatcher,
val continuation: Continuation<T>
) : DispatchedTask<T>(MODE_UNINITIALIZED),
Continuation<T> by continuation
所以这里实际上发生了一个非常重要的转换:
markdown
DispatchedContinuation
↓
Runnable
↓
交给 Dispatcher 调度
DispatchedContinuation 在这里并不是负责创建线程的。
它更像是给真正的 Continuation 包了一层调度逻辑,同时自己实现了 Runnable,这样它就可以被线程调度器当成一个普通任务来处理。
Dispatcher 最终把 Runnable 交给了 CoroutineScheduler
我们这次研究的是 Dispatchers.Default。
在 JVM 上,它最终会进入类似这样的实现:
kotlin
override fun dispatch(
context: CoroutineContext,
block: Runnable
) {
coroutineScheduler.dispatch(block)
}
这里已经非常直观了。
Dispatcher 并没有直接执行:
scss
block.run()
而是把这个 Runnable 交给了:
CoroutineScheduler
于是我们的调用链变成:
markdown
DispatchedContinuation
↓
Runnable
↓
Dispatchers.Default
↓
CoroutineScheduler
接下来问题就变成了:
CoroutineScheduler 收到这个 Runnable 之后怎么办?
Runnable 被包装成了 Task
CoroutineScheduler 的 dispatch():
kotlin
fun dispatch(
block: Runnable,
taskContext: TaskContext = NonBlockingContext,
fair: Boolean = false
) {
trackTask()
val task = createTask(block, taskContext)
val isBlockingTask = task.isBlocking
val stateSnapshot =
if (isBlockingTask) incrementBlockingTasks() else 0
val currentWorker = currentWorker()
val notAdded =
currentWorker.submitToLocalQueue(task, fair)
if (notAdded != null) {
if (!addToGlobalQueue(notAdded)) {
throw RejectedExecutionException(
"$schedulerName was terminated"
)
}
}
if (isBlockingTask) {
signalBlockingWork(stateSnapshot)
} else {
signalCpuWork()
}
}
这里首先做了一件很简单但容易被忽略的事情:
ini
val task = createTask(block, taskContext)
也就是说:
arduino
Runnable
↓
Task
为什么 Scheduler 不直接保存 Runnable?
因为 Scheduler 还需要知道这个任务是不是 blocking task,并据此维护自己的调度状态。
不过对于我们现在的主题来说,可以先把它简单理解成:
Scheduler 给 Runnable 又包了一层 Task。
Task 会进入哪个队列?
接下来是:
ini
val currentWorker = currentWorker()
val notAdded =
currentWorker.submitToLocalQueue(task, fair)
submitToLocalQueue() 的实现:
kotlin
private fun Worker?.submitToLocalQueue(
task: Task,
fair: Boolean
): Task? {
if (this == null) return task
if (state === WorkerState.TERMINATED) return task
if (!task.isBlocking &&
state === WorkerState.BLOCKING
) {
return task
}
mayHaveLocalTasks = true
return localQueue.add(task, fair = fair)
}
这里有一个很有意思的设计。
如果当前线程本身就是 CoroutineScheduler 的 Worker,那么它可能拥有自己的 LocalQueue。
于是:
csharp
localQueue.add(task)
把 Task 放入当前 Worker 的本地队列。
如果当前线程不是 Scheduler 的 Worker:
kotlin
if (this == null) return task
那么 Task 就会返回给 dispatch():
scss
if (notAdded != null) {
addToGlobalQueue(notAdded)
}
于是 Task 会进入 GlobalQueue。
所以这里形成了两条路径:
scss
CoroutineScheduler
│
currentWorker()
/ \
/ \
当前是 Worker 不是 Worker
│ │
↓ ↓
LocalQueue GlobalQueue
需要特别注意:
到这里还没有发生线程切换。
我们只是把一个任务放进了队列。
谁会把 Task 从队列里取出来?
现在我们终于来到了真正的 Worker。
Worker 本身是一个线程执行体,它的 run() 很简单:
kotlin
override fun run() = runWorker()
而 runWorker() 会不断寻找任务:
kotlin
private fun runWorker() {
while (!isTerminated &&
state != WorkerState.TERMINATED) {
val task = findTask(mayHaveLocalTasks)
if (task != null) {
executeTask(task)
continue
}
// 没找到任务,进入等待
// ...
}
}
这里就出现了我们之前一直追踪的另一个关键点:
scss
findTask()
它会从 Worker 的本地队列、全局队列或者其他 Worker 的队列中寻找任务。
例如:
kotlin
private fun findAnyTask(
scanLocalQueue: Boolean
): Task? {
if (scanLocalQueue) {
// ...
localQueue.poll()?.let {
return it
}
pollGlobalQueues()?.let {
return it
}
} else {
pollGlobalQueues()?.let {
return it
}
}
return trySteal(STEAL_ANY)
}
这里:
scss
localQueue.poll()
只是从队列里取出一个 Task。
这一点非常重要:
poll()本身并不会发生线程切换。
它只是:
arduino
Worker B
↓
从队列取出 Task
真正重要的是:执行这个 Task 的 Worker 是谁。
真正执行 Task
找到 Task 后:
scss
executeTask(task)
对于普通的 non-blocking Task,最终就是:
kotlin
private fun executeTask(task: Task) {
// ...
if (task.isBlocking) {
// blocking task 的特殊处理
// ...
} else {
runSafely(task)
}
}
而 runSafely():
kotlin
fun runSafely(task: Task) {
try {
task.run()
} catch (e: Throwable) {
val thread = Thread.currentThread()
thread.uncaughtExceptionHandler
.uncaughtException(thread, e)
} finally {
unTrackTask()
}
}
终于到了:
scss
task.run()
Task.run() 又做了什么?
这里其实没有什么神秘的东西。
Scheduler 创建的 Task 是 TaskImpl:
kotlin
private class TaskImpl(
@JvmField val block: Runnable,
submissionTime: Long,
taskContext: TaskContext
) : Task(submissionTime, taskContext) {
override fun run() {
block.run()
}
}
还记得之前传进 Scheduler 的 block 是什么吗?
就是:
DispatchedContinuation
因为:
yaml
DispatchedContinuation : Runnable
所以这里实际上就是:
scss
TaskImpl.run()
↓
block.run()
↓
DispatchedContinuation.run()
Task 这一层到这里就结束了。
回到了 DispatchedContinuation
现在我们又回到了最开始见过的:
scss
DispatchedContinuation.run()
它会取出之前保存的恢复结果,然后调用真正的 Continuation:
lua
continuation.resume(...)
或者:
scss
continuation.resumeWithException(...)
于是最终又回到了我们之前研究过的 Continuation 链:
scss
DispatchedContinuation.run()
↓
真正的 Continuation.resume()
↓
BaseContinuationImpl.resumeWith()
↓
Continuation 链
↓
协程状态机
↓
继续执行协程代码
到这里,整个过程就闭环了。
那么,线程究竟是怎么切换的?
现在我们可以重新回答最开始的问题。
假设协程原来运行在线程 A:
css
线程 A
某个事件触发了协程恢复。
如果当前 Dispatcher 判断需要调度:
arduino
线程 A
↓
DispatchedContinuation
↓
Dispatchers.Default
↓
CoroutineScheduler
↓
Task
↓
LocalQueue / GlobalQueue
随后某个 Worker 在线程 B 上运行:
scss
线程 B
↓
Worker.run()
↓
findTask()
↓
executeTask()
↓
task.run()
↓
DispatchedContinuation.run()
↓
Continuation.resume()
↓
协程状态机继续执行
所以我们真正看到的并不是:
css
线程 A
↓
某个 API
↓
切换到线程 B
而是:
arduino
线程 A
│
│ 提交恢复任务
↓
CoroutineScheduler
│
│ 保存 Task
↓
LocalQueue / GlobalQueue
│
│
│ 线程 B
│ ↓
└────→ Worker
↓
取出 Task
↓
执行 Task
↓
DispatchedContinuation.run()
↓
协程继续执行
线程切换的本质,是"提交任务的线程"和"最终执行任务的 Worker 所在线程"可以不同。
那为什么有时候又不会切换线程?
这也解释了我们最开始看到的:
kotlin
if (dispatcher.safeIsDispatchNeeded(context)) {
dispatcher.safeDispatch(context, this)
} else {
// 直接恢复
}
如果:
scss
isDispatchNeeded(context)
返回 false,那么就不需要把 Runnable 再提交给 Scheduler。
也就是说,当前线程可以直接继续执行。
因此:
scss
需要 Dispatch
↓
Runnable → Scheduler → Worker → run()
↓
可能换线程
而:
lua
不需要 Dispatch
↓
当前线程直接 resume
↓
继续执行
这也是为什么我们不能简单地把:
Dispatchers.Default = 线程切换
画等号。
更准确的说法是:
Dispatcher 决定恢复是否需要经过调度,以及任务最终由哪个线程执行。
最后把整个源码链串起来
经过这次源码追踪,我们可以得到这样一条完整的链:
scss
Continuation.resume
↓
DispatchedContinuation
↓
isDispatchNeeded()
↓
dispatch(context, Runnable)
↓
CoroutineScheduler.dispatch()
↓
Runnable → Task
↓
LocalQueue / GlobalQueue
↓
Worker.findTask()
↓
Worker.executeTask()
↓
runSafely(task)
↓
Task.run()
↓
block.run()
↓
DispatchedContinuation.run()
↓
Continuation.resume()
↓
BaseContinuationImpl.resumeWith()
↓
协程状态机继续执行
看完这条链之后,再回头看 DispatchedContinuation 会发现,它其实处在一个非常关键的位置:
markdown
协程世界
│
↓
DispatchedContinuation
│
↓
Runnable
│
↓
CoroutineScheduler
│
↓
Worker
│
↓
JVM线程
它把 Continuation 的恢复 和 线程池的任务调度 接了起来。
而这也是我们这次研究中我觉得最有意思的地方:
CoroutineScheduler 并不认识"协程"。
它看到的只是:
Runnable
经过一层包装之后:
arduino
Runnable → Task
然后 Worker 取出来:
arduino
Task.run()
最终又变回:
scss
Runnable.run()
而这个 Runnable 恰好就是:
DispatchedContinuation
于是一个看起来很复杂的"协程切换线程",最终在底层变成了一件非常朴素的事情:
把一个 Runnable 交给线程池,让另一个线程执行它。
至于 DispatchedContinuation 里的 dispatcher 到底是从哪里来的,以及 ContinuationInterceptor 又是如何参与这个过程的,这是另外一个问题。
这次我们先到这里。