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 ≠ ThreadDispatcher 决定的是:
协程应该由谁调度而不是:
具体在哪个线程执行二、先理解一个问题
例如:
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 协程三大启动方式的设计思想。
