高并发场景下的协程调度与调优
一、 前言
Dispatchers.Default
,Dispatchers.IO
,Dispatchers.Main
的线程池底层区别
Dispatchers.Default:适用于
CPU密集型任务
(计算、JSON解析等),其线程池大小为CPU核心数
到2X核心数
之间的固定线程池,调度策略是基于ForkJoinPool
或 kotlin的CommonPool
。如果线程不够,会挂起协程而非扩展线程。Dispatcher.IO: 适用于
IO密集型任务
(网络请求、数据库、磁盘读写),其线程池大小为无限制线程池
(实际64 X 核心数),底层上复用了Default
线程池,但是允许更多协程切入。适合同时发起大量短时间的IO任务,如果IO操作阻塞时间长,线程会被挂起,释放资源给其他协程。如果把CPU密集任务跑在IO上,可能导致线程数膨胀,调度开销大,甚至OOM。Dispatchers.Main:适用于所有需要与
UI
交互的任务,严格只有一个线程(UI线程),执行时间必须短,超过16ms可能会掉帧,避免 delay()、withContext(IO) 这类长时间操作。
- withContext() vs launch() 的调度差异
特性 | withContext() | launch() |
---|---|---|
是否为挂起函数 | 必须是suspend | 否 |
是否阻塞当前协程 | 会等待执行完,返回后继续 | 不会阻塞,异步执行 |
是否返回结果 | 有返回值(像普通函数一样) | 没有返回值(只能依靠回调或共享变量) |
异常传播 | 抛出异常给调用者,可以捕获 | 在同一个CoroutineScope会联动取消 |
是否是结构化并发 | 是 | 是 |
常见用途 | 切换线程并执行任务 + 获取结果 | 启动一个协程处理异步任务 |
适用总结:
- 需要结果 → 用 withContext()
- 只需要并发启动任务 → 用 launch()
- 复杂任务组合 → async + await 更合适
- 大量 withContext() 嵌套需慎用,易调度开销过大
示例如下:
方法一:用 withContext()
穿行处理(低效)
kotlin
suspend fun loadDataSerial(): UserOrderInfo {
val user = withContext(Dispatchers.IO) {
getUserInfo() // 网络请求
}
val orders = withContext(Dispatchers.IO) {
getOrderList(user.id) // 数据库加载或网络请求
}
return UserOrderInfo(user, orders)
}
缺点:两个请求是串行的,第二个必须等第一个完成 → 总耗时 = A + B
,不适合高并发聚合场景。
方法二:用 async + await 并发处理(高效)
kotlin
suspend fun loadDataConcurrent(): UserOrderInfo = CoroutineScope {
val userDefered = async(Dispatchers.IO) {
getUserInfo()
}
val ordersDeferred = async(Dispatchers.IO) {
val user = userDefered.await()
getOrderList(user.id)
}
val user = userDefered.await()
val orders = ordersDeferred.await()
UserOrderInfo(user, orders)
}
总耗时 ≈ Max(A, B)
以上两种方法的区别在于 async 启动的是 独立协程
,而 withContext() 是“切线程 + 等执行完再返回”
二、调度策略分析:Default vs IO vs Unconfined
三、SharedFlow、Channel 高并发背压
四、协程性能调优技巧
- 尽量减少 withContext(IO) 嵌套调用,切换开销大
- 大量计算使用 Default,但也要限流(Semaphore、Chunk 分片)
- 利用 SupervisorScope 局部处理异常防止协程传播 cancel
五、调试与排查
DebugProbes.install() + DebugProbes.dumpCoroutines()
打印活跃协程信息