—— 从线程池开始彻底讲透 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) 不一样这个面试特别喜欢问。launchlaunch(IO)返回Job特点开启新协程withContextwithContext(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 协程三大启动方式的设计思想。