调度器与执行上下文
源: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.Main | UI 操作(Android/Swing) | 平台相关的单线程(UI 线程) |
Dispatchers.IO | 阻塞式 I/O(文件读写、网络请求) | 共享线程池,默认最大 64 线程或 CPU 核心数取大者 |
Dispatchers.Default | CPU 密集型任务(算法、解析 JSON) | 共享线程池,线程数等于 CPU 核心数(最小 2) |
Dispatchers.Unconfined | 不限定线程(实验室特性) | 在当前线程启动,直到第一个挂起点 |
深度解析:IO 与 Default 的共享关系
在 JVM 上,Dispatchers.IO 和 Dispatchers.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()) // 依然有效
}
}核心准则总结
- 不要在
Main调度器做耗时操作:避免造成 UI 卡顿。 - IO 任务必须切到
Dispatchers.IO:防止阻塞计算线程池。 - 理解上下文的继承性:子协程会自动继承父协程的上下文,除非显式覆盖。