【Kotlin 协程修仙录 · 大乘境 · 中阶】 | 源码洞天:CoroutineScheduler 与 DispatchedContinuation

62 阅读7分钟

image_11.png

前言

大乘初成,你已跳出 Android 三界,俯瞰协程在 KMM、后端、JS 平台上的跨平台之道。你已知其然,亦知其所以然。

然而,真正的剑仙,不仅要能用剑,更要懂得剑的铸造之法。协程的源码,便是这把神剑的铸造图谱。当你深入 CoroutineSchedulerdispatch 方法,当你追踪 DispatchedContinuation 的拦截逻辑,协程的一切魔法都将褪去面纱,化作一行行清晰的代码。

  • 1、Dispatchers.IODispatchers.Default 共享的线程池到底是如何调度任务的?
  • 2、withContext 切换线程时,DispatchedContinuation 是如何拦截并改变执行线程的?
  • 3、协程从 launch 到真正执行,中间经历了哪些步骤?
  • 4、CoroutineSchedulerWorker 线程是如何偷取任务的?

这些问题,只有深入源码才能找到终极答案。本讲是大乘境的中阶修炼。我们将手持源码这把钥匙,打开协程内核的洞天:

  • 逐行剖析 CoroutineScheduler 的任务队列与 Worker 线程的工作窃取算法。
  • 追踪 DispatchedContinuationresumeWith,看透调度拦截的本质。
  • 理清协程从 launch 到状态机首行代码的完整启动流程。
  • 理解 CoroutineDispatcherisDispatchNeeded 如何决定是否切换线程。

准备好踏入源码洞天,与协程的底层实现正面交锋了吗?我们开始。

千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意


CoroutineScheduler:协程世界的线程池霸主

CoroutineScheduler 的整体架构

CoroutineScheduler 是 Kotlin 协程在 JVM 平台上的默认线程池实现。它同时服务于 Dispatchers.DefaultDispatchers.IO。它的设计目标是:用尽可能少的线程,支撑尽可能多的协程任务

其核心由两部分组成:

  • Worker 线程数组:固定数量的 Worker 实例,每个 Worker 是一个 Thread
  • 任务队列:分为全局队列和每个 Worker 的本地队列
flowchart LR
    subgraph Scheduler[CoroutineScheduler]
        direction TB
        GlobalQ[全局任务队列]
        Workers[Worker 线程数组]
    end
    
    subgraph Worker1[Worker 1]
        W1[Thread]
        LQ1[本地队列]
    end
    
    subgraph Worker2[Worker 2]
        W2[Thread]
        LQ2[本地队列]
    end
    
    GlobalQ --> Workers
    Workers --> Worker1
    Workers --> Worker2
    
    style Scheduler fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
    style Worker1 fill:#e3f2fd,stroke:#1976d2
    style Worker2 fill:#e3f2fd,stroke:#1976d2
    style GlobalQ fill:#ffb74d
    style LQ1 fill:#90caf9
    style LQ2 fill:#90caf9

核心类关系(简化):

internal class CoroutineScheduler(
    val corePoolSize: Int,      // CPU 核心数
    val maxPoolSize: Int,       // 最大线程数(IO 可扩容至此)
    val idleWorkerKeepAliveNs: Long
) : Executor, Closeable {
    // Worker 线程数组
    private val workers = arrayOfNulls<Worker>(maxPoolSize)
    // 全局任务队列
    private val globalQueue: GlobalQueue = GlobalQueue()
    
    fun dispatch(block: Runnable, taskContext: TaskContext = NonBlockingContext): Boolean
    private fun createNewWorker(): Worker
}

任务类型:CPUIO 的分野

CoroutineScheduler 区分两种任务类型:

任务类型TaskMode特点
CPU_BOUNDTaskMode.NON_BLOCKINGDispatchers.Default 提交,假定任务不会阻塞。调度器尽量保持每个核心一个活跃线程。
IO_BOUNDTaskMode.PROBABLY_BLOCKINGDispatchers.IO 提交,假定任务可能阻塞。当线程因阻塞而无法处理任务时,调度器会创建新线程来维持吞吐量。
flowchart LR
    subgraph Default[Dispatchers.Default]
        D1[CPU_BOUND 任务]
    end
    
    subgraph IO[Dispatchers.IO]
        I1[IO_BOUND 任务]
    end
    
    D1 --> Scheduler[CoroutineScheduler]
    I1 --> Scheduler
    
    Scheduler --> CPUPool[固定核心线程]
    Scheduler --> IOPool[弹性扩容线程]
    
    style Default fill:#c5e1a5,stroke:#558b2f
    style IO fill:#a5d6a7,stroke:#1b5e20
    style Scheduler fill:#fff3e0,stroke:#f57c00
    style CPUPool fill:#90caf9
    style IOPool fill:#81c784

工作窃取算法

每个 Worker 线程优先处理自己本地队列中的任务。当本地队列为空时,它会尝试从全局队列获取任务。如果全局队列也为空,它会随机窃取其他 Worker 本地队列中的任务。

// Worker 的 run() 方法简化逻辑
override fun run() {
    while (!isTerminated) {
        val task = localQueue.poll()           // 1. 先看本地
            ?: globalQueue.poll()              // 2. 再看全局
            ?: stealFromOtherWorker()          // 3. 最后窃取
        
        if (task != null) {
            task.run()
        } else {
            // 无任务,可能进入等待或终止
            tryPark()
        }
    }
}
deepseek_mermaid_20260902_9594ab.png

工作窃取算法保证了负载均衡——即使某些 Worker 的任务特别多,空闲的 Worker 也能主动分担,提高整体吞吐量。


DispatchedContinuation:线程切换的核心拦截器

Continuation 拦截机制回顾

在炼气境巅峰,我们学过 CPS 续体传球。Continuation 代表挂起函数的“剩余部分”。而 DispatchedContinuationContinuation 的一个特殊包装,它在 resumeWith不直接执行,而是将执行任务分派给调度器

internal class DispatchedContinuation<in T>(
    val dispatcher: CoroutineDispatcher,
    val continuation: Continuation<T>
) : Continuation<T> {
    
    override fun resumeWith(result: Result<T>) {
        // 关键:不直接调用 continuation.resumeWith,而是交由 dispatcher 调度
        if (dispatcher.isDispatchNeeded(context)) {
            dispatcher.dispatch(context, DispatchTask(continuation, result))
        } else {
            // 无需调度,直接在当前线程执行
            continuation.resumeWith(result)
        }
    }
}

withContext 的线程切换全流程

当你调用 withContext(Dispatchers.IO) { ... } 时,发生了以下步骤:

  1. withContext 创建一个新的 DispatchedContinuation,其 dispatcherDispatchers.IO
  2. 挂起当前协程,并将 DispatchedContinuation 返回给调度器。
  3. 调度器将 DispatchedContinuation 的任务(DispatchTask)提交给 Dispatchers.IO 的线程池。
  4. Dispatchers.IO 的某个线程执行该任务时,调用 continuation.resumeWith,从而恢复状态机的执行。
sequenceDiagram
    participant Caller as 调用方协程(主线程)
    participant WC as withContext
    participant DC as DispatchedContinuation
    participant Dispatcher as Dispatchers.IO
    participant Worker as Worker 线程

    Caller->>WC: withContext(IO)
    WC->>DC: 创建 DispatchedContinuation
    WC-->>Caller: 挂起,返回 COROUTINE_SUSPENDED
    DC->>Dispatcher: dispatch(DispatchTask)
    Dispatcher->>Worker: 任务入队
    Worker->>Worker: 执行任务
    Worker->>DC: resumeWith(结果)
    DC->>Caller: 恢复状态机执行

isDispatchNeeded:性能优化的关键

CoroutineDispatcherisDispatchNeeded 方法决定是否需要切换线程。例如,Dispatchers.Main.immediateisDispatchNeeded 会在当前已是主线程时返回 false,从而同步执行任务,避免一次无意义的 post

// Dispatchers.Main.immediate 简化逻辑
override fun isDispatchNeeded(context: CoroutineContext): Boolean {
    return !isMainThread() // 如果已在主线程,则无需 dispatch
}

协程启动全流程:从 launch 到状态机首行代码

launch 调用链追踪

当你写下 launch { ... },背后发生了什么?

// 简化调用链
CoroutineScope.launch(context, start) { block }
    -> AbstractCoroutine.start(start, this, block)
        -> CoroutineStart.invoke(block, receiver, completion)
            -> 根据 start 模式,最终调用
                -> block.startCoroutineCancellable(receiver, completion)
                    -> 创建状态机对象
                    -> 状态机.invokeSuspend()

启动流程图

deepseek_mermaid_20260902_e29413.png


实战:自定义一个简单的单线程调度器

理解了 CoroutineDispatcher 的拦截机制,我们可以手写一个自定义调度器,将所有任务串行在单一线程上执行。

import kotlinx.coroutines.*
import java.util.concurrent.Executors
import kotlin.coroutines.CoroutineContext

class SingleThreadDispatcher : CoroutineDispatcher() {
    private val executor = Executors.newSingleThreadExecutor { r ->
        Thread(r, "SingleThreadDispatcher")
    }

    override fun dispatch(context: CoroutineContext, block: Runnable) {
        executor.execute(block)
    }

    fun close() {
        executor.shutdown()
    }
}

// 使用示例
fun main() = runBlocking {
    val dispatcher = SingleThreadDispatcher()
    val scope = CoroutineScope(dispatcher)
    
    repeat(5) { i ->
        scope.launch {
            println("Task $i running on ${Thread.currentThread().name}")
            delay(100)
        }
    }
    
    delay(1000)
    dispatcher.close()
}

所有任务都会打印相同的线程名 SingleThreadDispatcher,证明它们在同一个线程上串行执行


常见误区与避坑指南

误区 1:认为 Dispatchers.IODefault 是完全独立的线程池

实际上它们共享 CoroutineScheduler,只是任务类型标记不同。误用可能导致线程池膨胀或性能退化。

误区 2:在自定义调度器中忘记 isDispatchNeeded 优化

如果你的调度器在某些情况下不需要切换线程,应重写 isDispatchNeeded 返回 false,以提升性能。

误区 3:误解 dispatch 方法的线程安全性

dispatch 可能被多线程并发调用,实现时必须保证线程安全(如使用 ConcurrentLinkedQueuesynchronized)。


最佳实践

  1. 理解调度器的线程池机制:合理分配 IODefault 任务。
  2. 必要时使用 limitedParallelism:限制特定操作的并发数。
  3. 自定义调度器记得实现 close 方法:释放线程资源。
  4. 调试时开启协程 Debug 模式:查看调度器分配的线程名称。
  5. 阅读源码CoroutineSchedulerDispatchedContinuation 是协程精髓所在。

总结与下回预告

恭喜,你已踏入源码洞天,看透了 CoroutineScheduler 的调度算法与 DispatchedContinuation 的拦截玄机!大乘境中阶修炼完成!

本讲核心收获

  • CoroutineScheduler 是协程默认线程池,采用工作窃取算法实现负载均衡。
  • DispatchedContinuation 通过 dispatch 将恢复任务交给调度器,实现线程切换。
  • withContext 本质上创建了带新调度器的 DispatchedContinuation
  • 协程启动从 launch 到状态机首行代码经历了多层调用与调度。

在下一讲 【大乘境·后阶】 中,我们将面对协程开发中的终极挑战:死锁排查内存模型。届时你将掌握:

  • 协程死锁的经典场景与排查工具。
  • Mutexsynchronized 在协程中的正确用法。
  • 协程与 ThreadLocal 的冲突及 asContextElement 的解决方案。

【当前境界修为面板】

当前境界修炼技能修炼进度修炼心得
大乘境 · 中阶1、CoroutineScheduler 源码透视镜
2、DispatchedContinuation 拦截真解
3、工作窃取算法诀
当前进度70%
修为700/1000
下一突破[大乘境 ·后阶] (需领悟:协程死锁排查、Mutex 用法、ThreadLocal 迁移)
Worker先看本地队列,再看全局,最后窃取兄弟。工作窃取是负载均衡的协程答案。

【本讲思考题】

  1. 表象题Dispatchers.IODispatchers.Default 是否共享同一个 CoroutineScheduler 实例?为什么?

  2. 场景题:你需要实现一个“任务优先级”调度器,高优先级任务优先执行。如何在自定义 CoroutineDispatcher 中实现?写出核心思路。

  3. 原理题DispatchedContinuationresumeWith 中,为什么要先调用 dispatcher.isDispatchNeeded?如果直接 dispatch 会有什么后果?


道友,大乘境的最后一道关隘已在眼前。掌握了死锁排查与内存模型,你的协程修为将真正圆满。大乘境·后阶见。

欢迎一键四连关注 + 点赞 + 收藏 + 评论