Kotlin 协程设计思想(一):CoroutineContext 到底是什么?为什么 Job 和 Dispatcher 可以直接相加?

引言

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 协程的生命周期管理机制。

相关推荐
JohnnyDeng9410 天前
【Android】ViewModelScope 与协程生命周期管理:告别内存泄漏,掌控异步边界
android·kotlin·mvvm·协程
至乐活着11 天前
Python异步编程asyncio完全指南:从入门到高性能实战
python·并发·协程·asyncio·异步编程
壮Sir不壮11 天前
GO语言——GMP调度模型
linux·开发语言·golang·go·操作系统·线程·协程
消失的旧时光-194314 天前
Kotlin 协程设计思想(十):Kotlin 协程到底解决了什么问题?
开发语言·kotlin·生命周期·rxjava·协程·结构化并发
消失的旧时光-194315 天前
Kotlin 协程设计思想(九):Flow 到底是什么?为什么 suspend 函数还需要 Flow?
android·kotlin·协程·协程异常
这是谁的博客?1 个月前
Python 异步编程核心原理与实践深度解析
java·网络·python·协程·asyncio·异步编程
.柒宇.1 个月前
Python 协程(Coroutine)指南:从入门到实战
python·协程
小书房1 个月前
Kotlin的协程
kotlin·高并发·协程·异步·虚拟线程·coroutinescope