Kotlin 协程设计思想(一):CoroutineContext 到底是什么?为什么 Job 和 Dispatcher 可以直接相加?
引言
CoroutineContext 本质上就是一个特殊的 Map
写 Kotlin 协程这些年,
有一段代码相信大家都写过:
private val scope = CoroutineScope( SupervisorJob() + Dispatchers.Default )或者:
viewModelScope.launch( Dispatchers.IO ) { }刚开始学协程的时候,我只是机械地记住:
Dispatchers.IO Dispatchers.Default Job SupervisorJob以及:
IO切线程 Job管理协程但一直有个问题没想明白:
为什么 Job 和 Dispatcher 能直接相加?
那个 + 到底干了什么?
直到最近重新梳理 Kotlin 协程体系,我才突然发现:
CoroutineContext 本质上就是一个特殊的 Map。而那个神秘的:
SupervisorJob() + Dispatchers.IO其实根本不是加法。
今天我们就彻底讲透 Kotlin 协程最核心的设计之一:
CoroutineContext
一、第一次看到这个代码时的疑惑
例如:
CoroutineScope( SupervisorJob() + Dispatchers.IO )很多人的第一反应都是:
Job 是任务管理器,Dispatcher 是线程调度器。这两个东西怎么相加?
如果放到 Java 世界:
job + dispatcher根本说不通。
因为:
它们不是同一种东西。那么 Kotlin 为什么允许这样写?
二、答案藏在 CoroutineContext 里面
先看 CoroutineScope 的构造函数:
public fun CoroutineScope( context: CoroutineContext ): CoroutineScope看到没有?真正传进去的不是:
Job也不是:
Dispatcher而是:
CoroutineContext问题来了:
CoroutineContext 又是什么?三、CoroutineContext 本质是什么?
很多教程会告诉你:
CoroutineContext 协程上下文说完就结束了。
但这句话其实非常抽象。
如果让我用一句最直白的话解释:
CoroutineContext ≈ 一个特殊的 Map例如:
Map<Key, Value>里面保存了协程运行所需要的各种配置。
四、Job 是一个配置项
例如:
SupervisorJob()实际上可以理解为:
Key = Job Value = SupervisorJob也就是说:
这是协程生命周期配置。五、Dispatcher 也是一个配置项
例如:
Dispatchers.IO实际上可以理解为:
Key = Dispatcher Value = IO Dispatcher表示:
协程应该运行在哪个线程池。六、那个 + 到底干了什么?
现在再来看:
SupervisorJob() + Dispatchers.IO实际上:不是加法
而是:Context 合并
可以理解成:
{ Job = SupervisorJob }加上:
{ Dispatcher = IO }最终得到:
{ Job = SupervisorJob Dispatcher = IO }这就是:
CoroutineContext七、为什么还能一直加?
例如:
CoroutineScope( SupervisorJob() + Dispatchers.IO + CoroutineName("Download") + CoroutineExceptionHandler { _, e -> } )最终得到:
CoroutineContext { Job = SupervisorJob Dispatcher = IO Name = Download ExceptionHandler = Handler }是不是特别像:
Map<String, Any>?
所以:
+ 其实是在不断往 Context 中增加配置。八、为什么后面的会覆盖前面的?
例如:
Dispatchers.IO + Dispatchers.Default最终生效的是:
Dispatchers.Default为什么?
因为:
Key 相同都属于:
Dispatcher所以:
后面的覆盖前面的就像:
mapOf( "name" to "张三", "name" to "李四" )最终:
name = 李四一样。
九、launch 到底干了什么?
例如:
CoroutineScope( SupervisorJob() + Dispatchers.IO + CoroutineName("Download") ).launch { }启动协程时,
协程会从 Context 中读取:
Job Dispatcher Name ExceptionHandler然后构建自己的运行环境。
也就是说:
launch() 不是简单创建协程 而是在创建一个协程运行环境。十、为什么 launch(Dispatchers.IO) 能切线程?
很多人天天写:
viewModelScope.launch( Dispatchers.IO ) { }以为:
切线程就结束了。
实际上:
viewModelScope本身已经有一个 Context:
{ Job Dispatcher(Main) }当你写:
launch(Dispatchers.IO)其实是:
父Context + { Dispatcher(IO) }得到:
{ Job Dispatcher(IO) }于是:
Main 被 IO 覆盖协程运行在 IO 线程池。
十一、SupervisorJob 为什么也放在 Context 里面?
以前我一直觉得:
SupervisorJob()是一个特殊工具类。
后来理解 Context 以后发现:
它其实只是:
CoroutineContext 中的一个配置项。作用是:
定义协程之间的父子关系。例如:
普通 Job一个子协程异常:
整个作用域取消而:
SupervisorJob则是:
一个子协程异常 不影响其它子协程十二、CoroutineContext 才是协程真正的核心
学协程时,很多人把注意力放在:
launch async withContext这些 API 上。
但实际上:
CoroutineContext 才是整个协程体系的根。因为:
Dispatcher Job CoroutineName ExceptionHandler全部都挂在 Context 上。
协程运行时,所有配置都来自:
CoroutineContext十三、最终总结
如果让我用一句话解释:
SupervisorJob() + Dispatchers.IO我会这样说:
不是把两个对象相加。 而是在组装一个协程运行环境。其中:
Job
负责生命周期Dispatcher
负责线程调度CoroutineName
负责调试ExceptionHandler
负责异常处理
而:CoroutineContext则负责把这一切组织在一起。
所以:
CoroutineContext 本质上不是一个对象。 而是一组协程配置的集合。理解了这一点,你才真正推开了 Kotlin 协程设计的大门。
下篇预告
既然 CoroutineContext 中最重要的配置之一是:
Job那么问题来了:
协程为什么可以取消? 父协程为什么能取消子协程? SupervisorJob 为什么不会连坐? 结构化并发到底是什么?下一篇我们继续:
《Kotlin 协程设计思想(二):Job 到底是什么?为什么协程能被取消?》
从 Job 树开始,彻底讲透 Kotlin 协程的生命周期管理机制。
