Skip to content

Kotlin 协程 Dispatchers、CoroutineContext 与构建器 ​

回到总览:并发与运行时底座
相关模块:Kotlin 协程并发总览 · Kotlin 协程 Scope 作用域、Job 状态机与异常隔离

一句话定义 ​

Kotlin 协程通过 CoroutineContext (上下文集合) 显式管理协程的运行元素,通过 CoroutineDispatcher (调度器) 决定协程切回哪条线程(如 Main / IO),并由 launch / async / withContext 等构建器控制协程的启动与并发等待。

代码索引 ​

对应 Lab:coroutine_execution_flow

为什么需要 ​

  • 为什么在 Android 中做耗时 I/O 操作必须显式切换到 Dispatchers.IO,而不能依赖 Dispatchers.Default?
    • 一句话答:Dispatchers.Default 适合 CPU 密集计算,其线程池大小严格受限于 CPU 核心数;而 Dispatchers.IO 是专门针对可能发生阻塞的 I/O 设定的,支持按需扩展至最多 64 条(或核心数大者)并发线程,避免阻塞计算线程池。
  • withContext 和 async { ... }.await() 在切换线程并等待结果时有什么本质区别?
    • 一句话答:withContext 会挂起当前协程并直接在目标 Dispatcher 上执行代码段,适合串行任务且不会创建额外 Job 对象;而 async 启动一个新的子协程,主要用于多任务并行再统一 await() 的场景。

底层机制 ​

1. 四大 Dispatcher 线程池调度矩阵 ​

2. CoroutineContext 组合与 Element 运算 ​

CoroutineContext 就像一个以 Element.Key 为索引的通用集合,支持 + 运算符组合:

val context = Dispatchers.IO + CoroutineName("MyTask") + Job()

Android / Flutter / Web / Backend 对照 ​

维度Kotlin 协程Dart (Flutter)Go 语言
线程调度Dispatchers.IO / Main / Default单线程 EventLoop (需 Isolate 多核)Goroutines (GMP 调度器自动调度)
切线程构建器withContext(Dispatchers.IO)compute() / Isolate.spawn()go func()
并行等待async {}.await()Future.wait([...])sync.WaitGroup / channel

常见场景 ​

1. 使用 withContext 实现主线程与 IO 线程安全切换 ​

kotlin
class UserRepository(private val api: UserApi) {
    suspend fun getUserDetails(userId: String): UserDetail = withContext(Dispatchers.IO) {
        // 自动切到 IO 线程池执行网络请求,执行完安全切回调用方线程
        api.fetchUser(userId)
    }
}

常见误配、事故后果与排障 ​

1. 事故:用 GlobalScope.launch 启动长任务,页面销毁后任务仍在跑 ​

  • 误配原因:为了"省得传 scope",直接 GlobalScope.launch { ... } 执行网络请求或写库。
  • 后果:GlobalScope 不绑定任何生命周期,页面销毁后协程照常执行;回调里访问已销毁的 View 直接崩溃(IllegalStateException),或造成资源泄漏、重复请求。
  • 排障与修法:改用绑定生命周期的作用域(viewModelScope / lifecycleScope / rememberCoroutineScope),让取消随页面传播;实在要全局任务,也必须自带超时与结果回调校验。

2. 事故:在 Dispatchers.Main 上直接做阻塞 I/O 或重计算 ​

  • 误配原因:suspend 函数内没切调度器,直接 File.readBytes() / 解析大 JSON。
  • 后果:suspend 不自动切换线程;阻塞操作占住主线程 → 掉帧、ANR。
  • 排障与修法:阻塞/重计算用 withContext(Dispatchers.IO) / Dispatchers.Default 包住;用 Stetho / Profiler 确认耗时确实离开了主线程。

3. 事故:滥用 Dispatchers.Unconfined 造成"线程漂移"幻觉 ​

  • 误配原因:听说 Unconfined "快",把 UI 更新逻辑也放进 Unconfined 协程。
  • 后果:Unconfined 挂起前在当前线程跑、恢复后跟随恢复线程跑;一旦中间有挂起,后续代码可能跑到任意线程上,线程归属完全不可预测,UI 更新时崩溃。
  • 排障与修法:Unconfined 只适合纯计算或协程启动开销敏感的极少数场景;涉及 UI / 共享状态一律显式指定 Dispatchers.Main / IO。

4. 事故:withContext 与 async 混用导致并发丢失 ​

  • 误配原因:写三个 withContext(Dispatchers.IO) { fetchX() } 以为"并行"。
  • 后果:withContext 是串行挂起,三个请求依次执行,总耗时 = 三者之和(详见复习题 2)。
  • 排障与修法:并行任务用 async {} 启动子协程 + awaitAll() 汇总;withContext 只用于"切线程 + 串行收口"。

对应实验 ​

各章节内已嵌入跳转链接;此处汇总全部 Lab:

Lab说明
coroutine_execution_flowMain → IO → Main 线程切换、async/await、状态机等效执行流验证

复习检查题 ​

  1. 在 Kotlin 协程中,Dispatchers.IO 与 Dispatchers.Default 是否共享底层的物理线程池?

    答:是的。在默认实现中,Dispatchers.IO 与 Dispatchers.Default 共享同一个底层物理线程池,但各自拥有独立的并发数上限控制。Default 的上限是 CPU 核心数(适合计算),而 IO 允许按需扩展至 64 条线程(适合阻塞 I/O),两者相互隔离配额。

  2. 为什么在需要并行发起 3 个网络请求时,应该用 async 而不是 withContext?

    答:因为 withContext 是同步挂起的,连续写 3 个 withContext 会导致 3 个网络请求串行依次执行;而 async 会并发启动 3 个独立的子协程并行发网络请求,最后通过 awaitAll() 统一等待结果,可以将总耗时从 3 次请求之和缩短为单次最长请求的耗时。

速记 ​

  • 四调度器:Main 负责 UI,Default 负责计算,IO 负责读写,Unconfined 不限制。
  • 构建器选型:串行切线程用 withContext,多任务并行用 async 后 await。
  • Context 组合:使用 + 运算符叠加 Job、Name 和 Dispatcher 元素。

站点构建时间:2026/8/24 23:43:17