调度器与线程:Dispatchers.Main、IO、Default、Unconfined详解
协程跑在哪个线程上?这个问题,说白了就是调度器在管。
我刚开始学协程时,以为它跟线程池差不多。后来踩了几个坑才明白——调度器是协程的「交通指挥员」,它决定了你的代码在哪个车道跑。
这节课,咱们把 Kotlin 提供的四个核心调度器掰开揉碎讲清楚。
Dispatchers.Main:主线程调度器
这个调度器专门用于 UI 操作。在 Android 上,它对应主线程;在桌面或服务端,它对应事件调度线程。
核心特点:
- 只能在有 Looper 的线程上运行
- Android 中默认绑定主线程
- 不适合做耗时操作
// 典型用法:更新 UI
lifecycleScope.launch(Dispatchers.Main) {
val result = fetchData() // 这里会报错!网络请求不能在主线程
textView.text = result
}
嗯,这里要注意。上面这段代码如果直接跑,会 crash。因为 fetchData() 如果是网络请求,Android 不允许在主线程执行。
正确的做法是:
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) { fetchData() }
textView.text = result // 自动回到 Main
}
我个人习惯把 Dispatchers.Main 当作「终点站」——所有 UI 更新都放在这里,但绝不在这里做耗时操作。
Dispatchers.IO:IO 密集型任务调度器
这个调度器是为网络请求、文件读写、数据库操作准备的。它的线程池会根据需要动态扩展。
我的一点经验:Dispatchers.IO 的线程数默认上限是 64(或 CPU 核心数 * 2,取较大值)。但别以为它无限大,阻塞太多还是会耗尽的。
// 网络请求的正确姿势
suspend fun loadUserData(): User {
return withContext(Dispatchers.IO) {
// 这里做网络请求或数据库查询
apiService.getUser()
}
}
我曾经在一个项目里,把所有耗时操作都丢进 Dispatchers.IO,结果发现某些场景下响应变慢了。排查后发现——IO 调度器的线程虽然多,但如果你同时发起几百个阻塞式文件读写,照样会排队。
避坑指南:不要把 CPU 密集计算丢进 IO 调度器。IO 调度器的线程数多,但每个线程的优先级较低。CPU 密集任务放这里反而会慢。
Dispatchers.Default:CPU 密集型任务调度器
这个调度器适合做计算密集型工作——比如图片处理、数据解析、复杂算法。它的线程数等于 CPU 核心数(最少 2 个)。
// 图片压缩的典型场景
suspend fun compressImage(bitmap: Bitmap): ByteArray {
return withContext(Dispatchers.Default) {
// 这里做 CPU 密集的压缩计算
compress(bitmap)
}
}
你想想看,为什么要有专门的 CPU 调度器?因为线程切换是有代价的。如果线程数超过 CPU 核心数,就会频繁发生上下文切换,反而降低效率。
Dispatchers.Default 的线程数刚好等于核心数,就是为了避免这种「过度并行」的问题。
| 调度器 | 适用场景 | 线程数 |
|---|---|---|
| Main | UI 更新、事件处理 | 1(主线程) |
| IO | 网络、文件、数据库 | 动态扩展,上限 64 |
| Default | 计算、解析、算法 | CPU 核心数 |
| Unconfined | 测试、特殊场景 | 不固定 |
Dispatchers.Unconfined:不限制调度器
这个调度器比较特殊。它不会把协程限制在特定线程上。协程在哪个线程被启动,就在哪个线程继续执行——直到遇到第一个挂起点。
// Unconfined 的行为演示
runBlocking {
launch(Dispatchers.Unconfined) {
println("开始于:${Thread.currentThread().name}")
delay(100) // 挂起点
println("恢复于:${Thread.currentThread().name}")
}
}
输出结果可能是:
开始于:main
恢复于:kotlinx.coroutines.DefaultExecutor
为什么会这样?因为 delay 之后,协程被恢复时,跑在了定时器的线程上。
注意:生产环境几乎不用 Dispatchers.Unconfined。它太不可控了。我一般只在写单元测试时用它,或者做一些简单的协程行为验证。
withContext:线程切换的瑞士军刀
withContext 是协程中最常用的线程切换工具。它不会创建新的协程,而是在当前协程的上下文中切换调度器。
// 经典三段式:IO 获取数据 → Default 处理数据 → Main 更新 UI
lifecycleScope.launch(Dispatchers.Main) {
val rawData = withContext(Dispatchers.IO) { fetchData() }
val processed = withContext(Dispatchers.Default) { processData(rawData) }
textView.text = processed
}
这段代码看起来像是串行的,但每个 withContext 都会切换到对应的线程池执行。执行完后自动回到 Dispatchers.Main。
性能小贴士:withContext 的切换开销很小。我做过压测,百万次切换也就几十毫秒。所以放心用,别为了「省几次切换」把代码写成一坨。
实战中的选择策略
说了这么多,到底怎么选?我总结了一个简单的判断流程:
- 要更新 UI? → 用
Dispatchers.Main - 要读写文件或网络? → 用
Dispatchers.IO - 要做复杂计算? → 用
Dispatchers.Default - 不确定? → 用
Dispatchers.Default(它是默认值)
我曾经接手过一个项目,里面大量使用了 Dispatchers.IO 做 JSON 解析。结果在低端手机上频繁卡顿。改成 Dispatchers.Default 后,流畅度明显提升。
原因很简单:JSON 解析是 CPU 密集操作,IO 调度器的线程优先级低,而且线程数多导致竞争激烈。换成 Default 后,线程数刚好等于核心数,每个线程都能充分利用 CPU。
我的习惯:在 ViewModel 中,我通常不显式指定调度器。而是让 Repository 层通过 withContext 切换。这样上层代码只关心业务逻辑,不关心线程细节。
好了,调度器这块就讲到这里。记住一句话:选对调度器,比优化代码更重要。线程用错了,再好的算法也白搭。