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

相关推荐
这是谁的博客?11 天前
Python 异步编程核心原理与实践深度解析
java·网络·python·协程·asyncio·异步编程
.柒宇.22 天前
Python 协程(Coroutine)指南:从入门到实战
python·协程
小书房24 天前
Kotlin的协程
kotlin·高并发·协程·异步·虚拟线程·coroutinescope
小书房24 天前
Kotlin协程的运行原理
android·开发语言·kotlin·协程
JohnnyDeng9425 天前
Kotlin 协程原理与 Android 中的最佳实践
android·kotlin·协程
小书房1 个月前
Java的虚拟线程
协程·异步·虚拟线程·coroutinescope
shao9185161 个月前
第3章(2)——使用Gradio JavaScript Client
javascript·node.js·cdn·gradio·job·events·playcode
亚林瓜子2 个月前
AWS Glue Python Shell任务中pip安装依赖库
python·shell·pip·aws·glue·job
鸿乃江边鸟2 个月前
Nanobot 从 Channel 消息处理看python协程的使用
人工智能·ai·协程