Appearance
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_flow | Main → IO → Main 线程切换、async/await、状态机等效执行流验证 |
复习检查题
在 Kotlin 协程中,
Dispatchers.IO与Dispatchers.Default是否共享底层的物理线程池?答:是的。在默认实现中,
Dispatchers.IO与Dispatchers.Default共享同一个底层物理线程池,但各自拥有独立的并发数上限控制。Default的上限是 CPU 核心数(适合计算),而IO允许按需扩展至 64 条线程(适合阻塞 I/O),两者相互隔离配额。为什么在需要并行发起 3 个网络请求时,应该用
async而不是withContext?答:因为
withContext是同步挂起的,连续写 3 个withContext会导致 3 个网络请求串行依次执行;而async会并发启动 3 个独立的子协程并行发网络请求,最后通过awaitAll()统一等待结果,可以将总耗时从 3 次请求之和缩短为单次最长请求的耗时。
速记
- 四调度器:Main 负责 UI,Default 负责计算,IO 负责读写,Unconfined 不限制。
- 构建器选型:串行切线程用
withContext,多任务并行用async后await。 - Context 组合:使用
+运算符叠加 Job、Name 和 Dispatcher 元素。