Kotlin 协程设计思想(三):Dispatchers 到底是什么?切线程真的只是切线程吗?

📅 2026/7/28 6:07:10 👁️ 阅读次数 📝 编程学习
Kotlin 协程设计思想(三):Dispatchers 到底是什么?切线程真的只是切线程吗?

—— 从线程池开始,彻底讲透 Dispatchers.IO、Dispatchers.Default、Dispatchers.Main 的设计思想

上一篇我们讲了:

《Job 到底是什么?为什么协程能被取消?》

我们知道:

CoroutineContext ≈ 特殊Map

其中:

Job 负责生命周期管理

而:

Dispatcher 负责协程调度

很多同学天天写:

launch(Dispatchers.IO) withContext(Dispatchers.IO) launch(Dispatchers.Default)

然后记住:

IO 处理网络 Default 处理计算 Main 更新UI

就结束了。

但我最近重新思考协程设计时突然发现:

Dispatchers 真的只是切线程吗?

答案是:

不是。

它实际上代表着:

协程运行策略(Scheduling Strategy)

而线程池只是其中一部分。


一、很多人对 Dispatcher 的理解其实是错的

第一次学协程时:

launch(Dispatchers.IO) { }

教程都会说:

切到IO线程

于是脑子里变成:

Dispatcher = Thread

事实上:

Dispatcher ≠ Thread

Dispatcher 决定的是:

协程应该由谁调度

而不是:

具体在哪个线程执行

二、先理解一个问题

例如:

launch { }

启动的是:

协程

不是:

线程

那么问题来了:

协程最终由谁执行?

答案:

Dispatcher

三、协程和线程到底是什么关系?

很多人容易混淆。


线程:

操作系统资源

例如:

Thread-1 Thread-2 Thread-3

真实存在。


协程:用户态任务

例如:

Coroutine A Coroutine B Coroutine C

本身不占用线程。


于是:

协程 ↓ Dispatcher ↓ 线程

形成关系。


四、Dispatcher 才是真正的调度者

例如:

launch(Dispatchers.IO) { }

实际过程:

Coroutine ↓ Dispatchers.IO ↓ 线程池 ↓ 某个线程执行

所以:

Dispatcher 是调度者

线程是执行者


五、Dispatchers.Main

最容易理解。

launch(Dispatchers.Main) { }

表示:

必须运行在主线程

例如:

textView.text = "Hello"

必须:

Main Dispatcher

否则:

CalledFromWrongThreadException

六、Dispatchers.IO

很多教程:

IO线程池

然后结束。

实际上 Dispatchers.IO 解决的是:

线程阻塞问题

例如:

networkApi.login()
File.readText()
database.query()

这些操作特点:

CPU不忙 线程在等待

例如:

等待网络 等待磁盘 等待数据库

此时:

线程被占着 却没干活

所以:

IO Dispatcher

允许创建更多线程。

默认:

64

或者:

CPU核心数

取较大值。


七、Dispatchers.Default

很多人以为:

Default就是普通线程池

其实:

Default专门处理CPU计算

例如:

for(i in 0..100000000) { }

或者:

图片压缩 JSON解析 加密解密 算法计算

特点:

线程一直忙

所以:

Default线程数:

CPU核心数

附近。

例如:

8核CPU ≈ 8个线程

为什么?

因为:

CPU计算 线程太多没意义

反而:

频繁上下文切换

更慢。


八、为什么 IO 和 Default 要分开?

这是 Kotlin 团队特别经典的设计。

假设只有一个线程池。


场景:

100个网络请求

全部占满线程。


这时候:

图片压缩

来了。

结果:

没有线程

只能排队。


反过来也一样。


所以:

Google直接拆成:

IO Dispatcher Default Dispatcher

把:

等待型任务

和:

计算型任务

彻底隔离。


九、withContext(IO) 真的是切线程吗?

很多人:

withContext(IO)

就认为:

切线程

实际上:

切的是Dispatcher

例如:

withContext(Dispatchers.IO)

本质:

创建新的CoroutineContext

相当于:

currentContext + Dispatchers.IO

然后:

Dispatcher覆盖

得到:

新的运行环境

十、为什么 launch(IO) 和 withContext(IO) 不一样?

这个面试特别喜欢问。


launch

launch(IO)

返回:

Job

特点:

开启新协程

withContext

withContext(IO)

特点:

不创建新协程

只是:

挂起当前协程

然后:

切换Dispatcher

执行完再回来。


所以:

launch = 开新车

withContext = 换车道


十一、为什么 Dispatchers 也放在 Context 里面?

上一篇讲过:

CoroutineContext ≈ 特殊Map

例如:

{ Job Dispatcher Name ExceptionHandler }

所以:

SupervisorJob() + Dispatchers.IO

实际上:

不是加法

而是:

配置协程运行环境

十二、你项目里其实天天在用

例如:

viewModelScope.launch { }

默认:

Main Dispatcher

然后:

withContext(IO) { }

网络请求。


再:

withContext(Main) { }

更新UI。


本质:

协程没变 Dispatcher在变

十三、CoroutineContext 和 Dispatcher 串起来了

上一篇:

CoroutineContext
是运行环境

这一篇:

Dispatcher
是运行环境中的调度器

所以:

CoroutineContext
决定协程怎么运行

Dispatcher
决定协程在哪运行


十四、最终总结

如果让我一句话解释:

Dispatchers.IO

我不会再说:

IO线程池

而会说:

处理阻塞型任务的调度策略

如果让我解释:

Dispatchers.Default

我会说:

处理CPU密集型任务的调度策略

如果让我解释:

Dispatchers.Main

我会说:

保证任务运行在UI线程的调度策略

所以:

Dispatcher 不是线程 而是协程与线程之间的调度桥梁。

下篇预告

现在:

CoroutineContext✓

Job✓

Dispatcher ✓

都讲完了。

那么问题来了:

launch async withContext 为什么要设计三种启动方式? 它们到底有什么区别? 为什么 async 的异常处理又不一样?

下一篇我们继续:

《Kotlin 协程设计思想(四):launch、async、withContext 到底有什么区别?》

从 Job、Deferred 到 结构化并发 ,彻底讲透 Kotlin 协程三大启动方式的设计思想。