第2章 协程调度全景图:调度器类型、线程池模型、并发原语
现象锚点:App 列表滑动卡顿、启动耗时陡增、ANR 日志中出现大量 “blocking in dispatcher” 或 “pool-1-thread-XX” 等待锁的栈轨迹。
排查经验表明,80% 的协程调度问题源于不理解调度器行为、线程池竞争或并发原语误用。
2.1 现象识别:调度层卡顿的典型表现
- UI 线程长时间被协程任务占满:在
Dispatchers.Main上执行 CPU 密集或阻塞 I/O,导致丢帧、ANR。 - 线程池饥饿:
Dispatchers.Default默认只有 2×CPU 核数个 worker,若大量协程挂起等待锁或长时间运行,新任务无法获得线程,整个上下文卡死。 - 并发原语死锁/活锁:
Mutex、Semaphore搭配不当,导致协程链永久挂起。 - Dispatcher 切换高频:频繁
withContext切换调度器,产生大量上下文切换开销,主线程响应变慢。
2.2 数据采集:抓取调度层证据
根据故障树,采集以下指标帮助锁定可疑节点:
| 采集工具 / 手段 | 目标数据 | 关键判据 |
|---|---|---|
| Android Studio Profiler (CPU / 线程) | 线程状态、调度器栈 trace | 大量协程处于 “Waiting” 且堆栈指向 DispatchedContinuation 或 CoroutineScheduler |
| dumpsys / strace | 线程数、锁等待 | 线程池大小超过预期,或大量线程阻塞在 park / unpark |
| 自定义协程监控(AOP 或字节码插桩) | 每次调度切换耗时、线程停留时间 | 单次 withContext > 5ms 即需关注 |
协程调试日志(DebugProbesKt) | 挂起帧关系、当前调度器类型 | 同一条链路中出现 3 次以上调度器切换 |
快速判断规则:若卡顿发生在主线程,优先排查
Dispatchers.Main 上的长时间任务;若卡顿发生在后台线程池,优先检查 Default 或 IO 的并发度与线程池耗尽情况。
2.3 原因定位:调度器、线程池模型与并发原语
定位三步走:
- 检查当前协程使用的调度器类型 —— 是否与任务特性匹配(CPU 密集型用
Default,I/O 密集型用IO,UI 操作用Main,定时器用Unconfined少用)。 - 分析线程池模型 ——
Default基于 Work‑Stealing 线程池,池大小固定(min(CPU 核数, 2));IO池默认最大 64 线程(可调),用于阻塞 I/O。若自定义 dispatcher 未复用已有池,可能造成线程泛滥或饥饿。 - 检查并发原语使用 ——
Mutex.withLock是否在非必要处加锁;Semaphore许可数设置是否小于任务并发需求;channels的onBufferOverflow策略是否导致协程挂起。
常见误用案例:
GlobalScope.launch(Dispatchers.Default) { … Thread.sleep(5000) } —— 长期占用 Default 线程,阻塞后续 CPU 任务。
正确做法:Dispatchers.IO 搭配 delay 或使用 withContext 临时切换。
2.4 修复动作:针对性调整调度配置
根据定位结果执行修复:
- 调度器误用修复:将阻塞 I/O 任务迁移到
Dispatchers.IO;将大量小计算任务改用Dispatchers.Default。 - 线程池模型调优:
或在开发阶段通过系统属性调整默认池大小:// 自定义 dispatcher,复用 IO 池或单独池 val myDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher() // 重要:记得在不用时 close() 避免泄漏 myDispatcher.close()// 修改 IO 池最大线程数(仅调试) System.setProperty("kotlinx.coroutines.io.parallelism", "32") - 并发原语解除死锁:
// 避免在协程中对 Mutex 双重加锁 class OrderService { private val mutex = Mutex() suspend fun process(order: Order) { withContext(Dispatchers.Default) { mutex.withLock { // 正确:只锁一次 // 长时间操作 } } } }
2.5 复测与验证
修改后重复数据采集步骤,确认以下指标达标:
- 主线程调度器任务持续时间 < 2ms(丢帧阈值)
Dispatchers.Default线程池活跃线程数不超过 poolSize,无剩余等待任务队列超过 10- 并发原语相关协程无死锁(可通过超时就崩溃的断言验证)
2.6 预防机制:调度层规范与监控
- 代码审查 checklist:每次
withContext或launch必须指定正确调度器,禁止在Main上使用阻塞 API。 - 单元测试中注入调度器:使用
TestCoroutineDispatcher模拟调度行为。 - 线上监控:对每次协程启动打点,记录调度器类型与执行时长,当协程执行超过 2s 时触发预警。
- 文档化线程池模型:在项目 wiki 中写明默认池配置、自定义池生命周期、并发原语使用约束。
本章小结
- 调度器是协程的“线程池选择器”,错误选择直接导致主线程阻塞或后台线程竞争。
- 线程池模型决定了并发容量与调度效率,线上需警惕 IO 池耗尽或自定义池未关闭。
- 并发原语必须与调度器配合使用,不能用
Mutex模拟信号量,也不能在单线程调度器上滥用锁。 - 本章提供的故障树、采集表和判断规则可直接用于分析线上卡顿问题。