引言
CoroutineContext 本质上就是一个特殊的 Map
写 Kotlin 协程这些年,
有一段代码相信大家都写过:
Kotlin
private val scope = CoroutineScope(
SupervisorJob() + Dispatchers.Default
)
或者:
Kotlin
viewModelScope.launch(
Dispatchers.IO
) {
}
刚开始学协程的时候,我只是机械地记住:
Kotlin
Dispatchers.IO
Dispatchers.Default
Job
SupervisorJob
以及:
IO切线程
Job管理协程
但一直有个问题没想明白:
为什么 Job 和 Dispatcher 能直接相加?
那个 + 到底干了什么?
直到最近重新梳理 Kotlin 协程体系,我才突然发现:
CoroutineContext
本质上就是一个特殊的 Map。
而那个神秘的:
SupervisorJob() + Dispatchers.IO
其实根本不是加法。
今天我们就彻底讲透 Kotlin 协程最核心的设计之一:
CoroutineContext
一、第一次看到这个代码时的疑惑
例如:
Kotlin
CoroutineScope(
SupervisorJob() +
Dispatchers.IO
)
很多人的第一反应都是:
Job 是任务管理器 ,Dispatcher 是线程调度器 。这两个东西怎么相加?
如果放到 Java 世界:
job + dispatcher
根本说不通。
因为:
它们不是同一种东西。
那么 Kotlin 为什么允许这样写?
二、答案藏在 CoroutineContext 里面
先看 CoroutineScope 的构造函数:
Kotlin
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
七、为什么还能一直加?
例如:
Kotlin
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 到底干了什么?
例如:
Kotlin
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 协程的生命周期管理机制。