Skip to content

调度器与执行上下文

源:Coroutine context and dispatchers

调度器(Dispatchers)决定了协程在哪个线程或线程池中执行。它是协程与 JVM 线程模型之间的桥梁。

CoroutineContext:协程的上下文字典

在深入调度器之前,必须理解 CoroutineContext。它是一个持久化的、不可变的键值对集合,定义了协程的行为。

核心签名

查看 CoroutineContext 接口
kotlin
public interface CoroutineContext {
    public operator fun <E : Element> get(key: Key<E>): E?
    public fun plus(context: CoroutineContext): CoroutineContext
    public interface Element : CoroutineContext
    public interface Key<E : Element>
}

上下文合并逻辑 (Context Combination)

当我们使用 scope.launch(context) 时,最终的上下文是 scope 的上下文与传入参数的合并结果:

kotlin
val scope = CoroutineScope(Job() + Dispatchers.Main)
// 结果上下文:Job + Dispatchers.IO + Name
scope.launch(Dispatchers.IO + CoroutineName("MyTask")) {
    // 这里的调度器是 IO
}
kotlin
// 左侧 + 右侧 -> 右侧元素会替换左侧同类型的元素
val context = Dispatchers.Default + CoroutineName("Old")
val newContext = context + CoroutineName("New")
// newContext 包含 CoroutineName("New")

标准调度器全家桶

Kotlin 官方提供了四种标准调度器,各自针对不同的工作负载。

调度器适用场景线程池特性
Dispatchers.MainUI 操作(Android/Swing)平台相关的单线程(UI 线程)
Dispatchers.IO阻塞式 I/O(文件读写、网络请求)共享线程池,默认最大 64 线程或 CPU 核心数取大者
Dispatchers.DefaultCPU 密集型任务(算法、解析 JSON)共享线程池,线程数等于 CPU 核心数(最小 2)
Dispatchers.Unconfined不限定线程(实验室特性)在当前线程启动,直到第一个挂起点

深度解析:IO 与 Default 的共享关系

在 JVM 上,Dispatchers.IODispatchers.Default 实际上共享同一个后台线程池,但它们使用了不同的 分派限制 (Limiter)

底层机制

Dispatchers.IO 允许通过 limitedParallelism 动态扩展并发数。

kotlin
// 为特定任务创建独立的并发限制
val myIoDispatcher = Dispatchers.IO.limitedParallelism(100)

切换上下文:withContext

withContext 是一个挂起函数,允许你在不破坏结构化并发的前提下,临时切换协程的执行环境。

withContext 的性能优势

async { ... }.await() 相比,withContext 更轻量:

  • 它不会创建新的 Job
  • 它在同一协程内进行上下文切换,减少了内存分配。

最佳实践:Main.immediate

在 Android 中,如果你当前已经在主线程,使用 Dispatchers.Main 会触发一个 handler.post 导致异步分派。而 Dispatchers.Main.immediate 会立即执行,避免不必要的帧延迟。

线程局部变量 (ThreadLocal) 集成

协程在不同线程间跳转时,普通的 ThreadLocal 会失效。Kotlin 提供了 asContextElement 来解决此问题。

kotlin
val myThreadLocal = ThreadLocal<String>()

scope.launch(myThreadLocal.asContextElement(value = "Hello")) {
    // 无论协程跳到哪个线程,myThreadLocal.get() 都能拿到 "Hello"
    withContext(Dispatchers.IO) {
        println(myThreadLocal.get()) // 依然有效
    }
}

核心准则总结

  1. 不要在 Main 调度器做耗时操作:避免造成 UI 卡顿。
  2. IO 任务必须切到 Dispatchers.IO:防止阻塞计算线程池。
  3. 理解上下文的继承性:子协程会自动继承父协程的上下文,除非显式覆盖。