Kotlin 协程源码解析:协程是如何切换线程的?

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

CoroutineSchedulerdispatch()

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 又是如何参与这个过程的,这是另外一个问题。

这次我们先到这里。

相关推荐
mmsx1 小时前
Android GIS系列 内存加锁、SQLite 开事务:矢量数据集双层并发设计
android·性能优化·app
Godikov1 小时前
没有 adb 怎么排查接口问题?给 OkHttp 加一个"永不炸"的文件日志拦截器
android
邪修king2 小时前
Re:Linux 系统篇(三十四):进程间通信开篇Chapter1 —— 匿名管道 pipe 深度拆解与代码实战
android·linux·运维·开发语言
AFinalStone2 小时前
Android7 SystemUI 源码解析(一)全景概览与架构设计
android·systemui
ai2work2 小时前
附录 D 速查索引与阅读指南
kotlin
ai2work2 小时前
ch22 诊断方法论:三个真实事故的根因分析
kotlin
2501_916008893 小时前
如何实现iOS App代码混淆:SwiftShield使用指南
android·ios·小程序·https·uni-app·iphone·webview
恋猫de小郭3 小时前
AndroidX 新增 Security State ,支持可编程的系统安全状态查询
android·前端·flutter
AFinalStone3 小时前
Android7 SystemUI 源码解析(六)QuickSettings快捷设置面板
android·systemui