第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 原因定位:调度器、线程池模型与并发原语

定位三步走:

  1. 检查当前协程使用的调度器类型 —— 是否与任务特性匹配(CPU 密集型用 Default,I/O 密集型用 IO,UI 操作用 Main,定时器用 Unconfined 少用)。
  2. 分析线程池模型 —— Default 基于 Work‑Stealing 线程池,池大小固定(min(CPU 核数, 2));IO 池默认最大 64 线程(可调),用于阻塞 I/O。若自定义 dispatcher 未复用已有池,可能造成线程泛滥或饥饿。
  3. 检查并发原语使用 —— 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 中写明默认池配置、自定义池生命周期、并发原语使用约束。
协程调度全景图 调度器类型 Dispatchers.Main Dispatchers.Default Dispatchers.IO Dispatchers.Unconfined 自定义调度器 线程池模型 Default 池 (Work‑Stealing) IO 池 (阻塞友好) 自定义 Executor 并发原语 Mutex (锁) Semaphore (信号量) Channel (通道) Flow / SharedFlow Job / SupervisorJob 委托执行 控制同步 问题排查映射 ① 卡顿现象 → ② 采集线程栈与调度器 trace → ③ 定位误用类型 → ④ 调整调度器/线程池/原语 → ⑤ 复测→⑥ 预防指南 常见故障与处置 Main 上阻塞 → 切 IO|Default;线程池饥饿 → 调 parallel/复用池;死锁 → 避免嵌套锁 【排查表】参见 2.2 节数据采集表

本章小结

  • 调度器是协程的“线程池选择器”,错误选择直接导致主线程阻塞或后台线程竞争。
  • 线程池模型决定了并发容量与调度效率,线上需警惕 IO 池耗尽或自定义池未关闭。
  • 并发原语必须与调度器配合使用,不能用 Mutex 模拟信号量,也不能在单线程调度器上滥用锁。
  • 本章提供的故障树、采集表和判断规则可直接用于分析线上卡顿问题。