前言
在 Review 代码时经常看到这样的写法
withContext(Dispatchers.IO) {
// 一些非UI操作
}
代码本意是想在把一些非UI任务放到后台,避免阻塞UI。但有时候开发者会忽略任务类型(CPU 密集型 or IO 密集型),无脑切到 IO 去做是不合理的。
Dispatchers.IO 的原理
顾名思义,Dispatchers.IO
是用于 IO 任务的协程调度器。IO 任务是一种不会给 CPU 带来高负载的工作,大部分时间都消耗在等待硬件响应(来自持久存储或远程主机)上
看一下源码:
// Dispatchers.IO
internal object DefaultIoScheduler : ExecutorCoroutineDispatcher(), Executor {
private val default = UnlimitedIoScheduler.limitedParallelism(
SystemProp(
IO_PARALLELISM_PROPERTY_NAME,
64.coerceAtLeast(AVAILABLE_PROCESSORS)
)
)
...
}
这里我们可以看到,在一般情况下,IO 调度器中的最大线程数是 64。如果我们在这个线程池上执行大量占用 CPU 的工作,对于 4 核或 8 核的 CPU 来说,这个数量可能就太多了
private object UnlimitedIoScheduler : CoroutineDispatcher() {
@InternalCoroutinesApi
override fun dispatchYield(context: CoroutineContext, block: Runnable) {
DefaultScheduler.dispatchWithContext(block, BlockingContext, true)
}
override fun dispatch(context: CoroutineContext, block: Runnable) {
DefaultScheduler.dispatchWithContext(block, BlockingContext, false)
}
...
}
这里所有任务都是通过 BlockingContext
进行调度的,内部实现如下:
if (isBlockingTask) {
// Dispatchers.IO 恒为 true
signalBlockingWork(stateSnapshot)
} else {
signalCpuWork()
}
对于 Dispatchers.IO
,这个标志始终为 true
,这意味着这个调度器是为阻塞调用优化的,用它来处理占用 CPU 密集型任务反而有损性能。
Dispatchers.IO 处理 CPU 任务会影响性能
通过上面代码可知:Dispatchers.IO
默认线程数较多,是弹性线程池,一般取 64 或 CPU 核心数中较高者。CPU 密集型任务主要靠 CPU 运算,过多线程会让 CPU 频繁切换线程上下文,增加额外开销,性能降低。比如有个计算任务,本来一个线程就能高效完成,结果 64 个线程抢着做,CPU 在这 64 个线程间切换,耗费大量时间在上下文切换,真正计算时间变少 。
Dispatchers.IO 专为 I/O 密集型任务设计,I/O 任务大部分时间在等外部资源响应,如网络请求等待服务器回复、文件读写等待硬盘传输数据。它针对这种等待场景优化,对于 CPU 密集型任务这种持续占用 CPU 计算资源的场景,没有优化适配,使用时效率不高。
Dispchers.Default 更适合做 CPU 任务
Dispatchers.Default
使用固定大小线程池,线程数量和 CPU 核心数对应。CPU 密集型任务在多核 CPU 上可并行执行多个线程或进程提升性能,其线程数与核心数匹配,能充分利用 CPU 核心,避免线程过多导致上下文切换开销大的问题。比如 8 核 CPU,Dispatchers.Default 线程池有 8 个线程,刚好每个核心分配一个线程执行计算任务,高效利用 CPU 资源 。
它专门为 CPU 密集型任务设计,任务调度等机制都是围绕 CPU 密集型任务特点优化的。相比 Dispatchers.IO 那种为 I/O 等待场景设计的调度器,Dispatchers.Default 在处理 CPU 密集型任务时,更能发挥 CPU 性能,提升任务执行效率 。
使用三方库时的注意
大家在实现诸如网络请求、读写文件、数据库查询等任务时,都会找各种三方库帮忙。像网络请求,常用 Retrofit 搭配 OkHttp;数据库操作呢,就用 Room。这些库已经在内部处理好了 IO 任务调用,用它们自己的线程来管理。
例如,我们调用 Retrofit 接口中用于 REST API 的挂起函数。OkHttp 内部已经有自己的 Dispatcher
和 ThreadPoolExecutor
来管理网络调用。所以,如果你把调用包裹在 withContext (Dispatchers.IO)
中,你只是把诸如准备请求和解析 JSON 这类占用 CPU 的工作交给了这个调度器,反而是画蛇添足。而所有真正的阻塞式 IO 其实是在 OkHttp 专用的线程池中进行的。
结论
Dispatchers.IO
处理占用 CPU 的任务会影响应用程序的性能,需要谨慎使用,在 IO 三方库都能自行处理任务调度的情况下,大多数情况下,如果你想在后台线程做些工作,应该使用 Dispatchers.Default
。