当前位置: 首页 > news >正文

用了20多张图终于把协程上下文CoroutineContext彻底搞懂了

1. 前言

如果你对CoroutineContext不了解,本文值得你细细品读,如果一遍看不懂,不妨多读几遍。写作该文的过程也是我对CoroutineContext理解加深的过程。CoroutineContext是协程的基础,值得投入学习

Android开发者对Context都不陌生。在Android系统中,Context可谓神通广大,它可以获取应用资源,可以获取系统资源,可以启动Activity。Context有几个大名鼎鼎的子类,Activity、Application、Service,它们都是应用中非常重要的组件。

协程中也有个类似的概念,CoroutineContext。它是协程中的上下文,通过它我们可以控制协程在哪个线程中执行,可以设置协程的名字,可以用它来捕获协程抛出的异常等。

我们知道,通过CoroutineScope.launch方法可以启动一个协程。该方法第一个参数的类型就是CoroutineContext。默认值是EmptyCoroutineContext单例对象。e5b0cebec8125a13a6d5c042e629b9bb.png

在开始讲解CoroutineContext之前我们来看一段协程中经常会遇到的代码20b4867e754c21ce352afffca3a01515.png

刚开始学协程的时候,我们经常会和Dispatchers.Main、Job、CoroutineName、CoroutineExceptionHandler打交道,它们都是CoroutineContext的子类。我们也很容易单独理解它们,Dispatchers.Main指把协程分发到主线程执行,Job可以管理协程的生命周期,CoroutineName可以设置协程的名字,CoroutineExceptionHandler可以捕获协程的异常。但是+操作符对大部分的Java开发者甚至Kotlin开发者而言会感觉到新鲜又难懂,在协程中CoroutineContext+到底是什么意思?

其实+操作符就是把两个CoroutineContext合并成一个链表,后文会详细讲解

2. CoroutineContext类图一览

85edd922b8a7703ee0fcb09a2159fd8c.png

根据类图结构我们可以把它分成四个层级:

  1. CoroutineContext 协程中所有上下文相关类的父接口。

  2. CombinedContext、Element、EmptyCoroutineContext。它们是CoroutineContext的直接子类。

  3. AbstractCoroutineContextElement、Job。这两个是Element的直接子类。

  4. CoroutineName、CoroutineExceptionHandler、CoroutineDispatcher(包含Dispatchers.Main和Dispatchers.Default)。它们是AbstractCoroutineContextElement的直接子类。

图中红框处,CombinedContext定义了size()和contains()方法,这与集合操作很像,CombinedContext是CoroutineContext对象的集合,而Element和EmptyCoroutineContext却没有定义这些方法,真正实现了集合操作的协程上下文只有CombinedContext,后文会详细讲解

3. CoroutineContext接口

CoroutineContext源码如下:b8ef968bd9ce60436f7920bc1c97b684.png首先我们看下官方注释,我将它的作用归纳为:

Persistent context for the coroutine. It is an indexed set of [Element] instances. An indexed set is a mix between a set and a map. Every element in this set has a unique [Key].

  1. CoroutineContext是协程的上下文。

  2. CoroutineContext是element的set集合,没有重复类型的element对象。

  3. 集合中的每个element都有唯一的Key,Key可以用来检索元素。

相信大多数的人看到这样的解释时,都会心生疑惑,既然是set类型为啥不直接用HashSet来保存Element。CoroutineContext的实现原理又是什么呢?原因是考虑到协程嵌套,用链表实现更好。

接着我们来看下该接口定义的几个方法8df810dc7318a50204e855dd9fa4708a.png

4. Key接口

dc9838939d44cc8878c7f5a93192a6b1.png

Key是一个接口定义在CoroutineContext中的一个接口,作为接口它没有声明任何的方法,那么其实它没有任何真正有用的意义,它只是用来检索。我们先来看下,协程库中是如何使用Key接口的。e65390f4a910b2739cdf29e86df0c380.png通过观察协程官方库中的例子,我们发现Element的子类都必须重写Key这个属性,而且Key的泛型类型必须和类名相同。以CoroutineName为例,Key是一个伴生对象,同时Key的泛型类型也是CoroutineName。

为了方便理解,我仿照写了MyElement类,如下:

f18ae78b67b0e3e3416626d9b9272993.png

通过对比kt类和反编译的java类我们看到 Key就是一个静态变量,而且它的实现类,其实啥也没干。它的作用与HashMap中的Key类似:

  1. 实现key-value功能,为插入和删除提供检索功能

  2. Key是static静态变量,全局唯一,为Element提供唯一性保障

Kotlin语法糖

coroutineContext.get(CoroutineName.Key)

coroutineContext.get(CoroutineName)

coroutineContext[CoroutineName]

coroutineContext[CoroutineName.Key]

写法是等价的

4. CoroutineContext.get方法

源码(整理在一起,下同)e23af162d5685a406325f1f8761debab.png

使用方式5d31f06b91e606317816ea2095fc31b5.png

讲解

通过Key检索Element。返回值只能是Element或者null,链表节点中的元素值。

  1. Element get方法:只要Key与当前Element的Key匹配上了,返回该Element否则返回null。

  2. CombinedContext get方法:遍历链表,查询与Key相等的Element,如果没找到返回null。

5. CoroutineContext.plus方法

源码e00bf941573d19cdab79d2a379b3965a.png

使用方式5895710730bebb2eaed6dcb0ce889c14.png

讲解

将两个CoroutineContext组合成一个CoroutineContext,如果是两个类型相同的Element会返回一个新的Element。如果是两个不同类型的Element会返回一个CombinedContext。如果是多个不同类型的Element会返回一条CombinedContext链表。

我将上述算法总结成了5种场景,不过在介绍这5种场景前,我们先讲解CombinedContext的数据结构。

6. CombinedContext分析

01cb6783900b53d1cf392778ca9efb7c.png

因为CombinedContext是CoroutineContext的子类,left也是CoroutineContext类型的,所以它的数据结构是链表。我们经常用next来表示链表的下一个节点。那么为什么这里取名叫left呢?我甚至怀疑写这段代码的是个左撇子。真正的原因是,协程可以启动子协程,子协程又可以启动孙协程。父协程在左边,子协程在右边

343241eec1bca573681f2aee29e3b07d.png

嵌套启动协程36701a08af524d322c2a78e83cb8a13d.png越是外层的协程的Context越在左边,大概示意图如下 (真实并非如此,比这更复杂)11e45d25732b210e39147606579df07a.png

链表的两个知识点在此都有体现。CoroutineContext.plus方法中使用的是头插法。CombinedContext的toString方法采用的是链表倒序打印法。

7. 五种plus场景

根据plus源码,我总结出会覆盖到五种场景。4a2519d31723a30d40ef1e1f585c9f5e.png

  1. plus EmptyCoroutineContext

  2. plus 相同类型的Element

  3. plus方法的调用方没有Dispatcher相关的Element

  4. plus方法的调用方只有Dispatcher相关的Element

  5. plus方法的调用方是包含Dispatcher相关Element的链表

结果如下:

  1. Dispatchers.Main + EmptyCoroutineContext 结果:Dispatchers.Main

  2. CoroutineName("c1") + CoroutineName("c2")结果: CoroutineName("c2")。相同类型的直接替换掉。

  3. CoroutineName("c1") + Job()结果:CoroutineName("c1") <- Job。头插法被plus的(Job)放在链表头部

  4. Dispatchers.Main + Job()结果:Job <- Dispatchers.Main。虽然是头插法,但是ContinuationInterceptor必须在链表头部。

  5. Dispatchers.Main + Job() + CoroutineName("c5")结果:Job <- CoroutineName("c5") <- Dispatchers.Main。Dispatchers.Main在链表头部,其它的采用头插法。

如果不考虑Dispatchers.Main的情况。我们可以把+<-代替。CoroutineName("c1") + Job()等价于CoroutineName("c1") <- Job

8. CoroutineContext的minusKey方法

源码067e24fbc95cd8ad4d40af320e66ca21.png

讲解

  1. Element minusKey方法:如果Key与当前element的Key相等,返回EmptyCoroutineContext,否则相当于没减成功,返回当前element

  2. CombinedContext minusKey方法:删除链表中符合条件的节点,分三种情况。

三种情况以下面链表为例

Job <- CoroutineName("c5") <-Dispatchers.Main

  1. 没找到节点:minusKey(MyElement)。在Job节点处走newLeft === left分支,依此类推,在CoroutineName处走同样的分支,在Dispatchers.Main处走同样的分支。

  2. 节点在尾部:minusKey(Job)。在CoroutineName("c5")节点走newLeft === EmptyCoroutineContext分支,依此往头部递归

  3. 节点不在尾部:minusKey(CoroutineName)。在Dispatchers.Main节点处走else分支

9.  总结

学习CoroutineContext首先要搞清楚各类之间的继承关系,其次,CombinedContext各具体Element的集合,它的数据结构是链表,如果读者对链表增删改查操作熟悉的话,那么很容易就能搞懂CoroutineContext原理,否则想要搞懂CoroutineContext那简直如盲人摸象。

相关文章:

  • 绝对干货,直接收藏 | 3D 可视化入门:渲染管线原理与实践
  • 视频播放器选择怎样的丢帧策略~~
  • Window 下 FFmpeg 和 LibX264 的编译和配置
  • 智能语音技术新进展与发展趋势
  • Android 系统中的文字渲染~
  • 微博HDR视频的落地实践
  • 年末总结 | 音视频开发进阶 2021 干货合集
  • Shadertoy 详解
  • Shadertoy 进阶 01
  • 拍乐云首发音视频「分组讨论」开放能力,开启线上群聊互动新玩法
  • 浅入浅出WebGPU
  • Vulkan 在 FFmpeg 中的支持
  • 音视频中的语音信号处理技术
  • 声网3D空间音频技术解析:3D空间音效+空气衰减模拟+人声模糊
  • 音视频春节假期内卷指南(实操)
  • java架构面试锦集:开源框架+并发+数据结构+大企必备面试题
  • laravel 用artisan创建自己的模板
  • MobX
  • Node + FFmpeg 实现Canvas动画导出视频
  • React-redux的原理以及使用
  • SpriteKit 技巧之添加背景图片
  • tab.js分享及浏览器兼容性问题汇总
  • v-if和v-for连用出现的问题
  • 对超线程几个不同角度的解释
  • 基于Javascript, Springboot的管理系统报表查询页面代码设计
  • 实战:基于Spring Boot快速开发RESTful风格API接口
  • 算法之不定期更新(一)(2018-04-12)
  • 我与Jetbrains的这些年
  • 学习笔记DL002:AI、机器学习、表示学习、深度学习,第一次大衰退
  • 一些css基础学习笔记
  • 找一份好的前端工作,起点很重要
  • ​无人机石油管道巡检方案新亮点:灵活准确又高效
  • ​一些不规范的GTID使用场景
  • ​总结MySQL 的一些知识点:MySQL 选择数据库​
  • ###STL(标准模板库)
  • #pragma once与条件编译
  • #我与Java虚拟机的故事#连载19:等我技术变强了,我会去看你的 ​
  • (3)Dubbo启动时qos-server can not bind localhost22222错误解决
  • (C#)Windows Shell 外壳编程系列9 - QueryInfo 扩展提示
  • (C++17) std算法之执行策略 execution
  • (JSP)EL——优化登录界面,获取对象,获取数据
  • (ros//EnvironmentVariables)ros环境变量
  • (附源码)spring boot基于Java的电影院售票与管理系统毕业设计 011449
  • (论文阅读26/100)Weakly-supervised learning with convolutional neural networks
  • (免费领源码)python#django#mysql公交线路查询系统85021- 计算机毕业设计项目选题推荐
  • (十八)三元表达式和列表解析
  • (十三)Maven插件解析运行机制
  • (十一)c52学习之旅-动态数码管
  • (数据结构)顺序表的定义
  • (四)图像的%2线性拉伸
  • (一)SpringBoot3---尚硅谷总结
  • (转)如何上传第三方jar包至Maven私服让maven项目可以使用第三方jar包
  • .babyk勒索病毒解析:恶意更新如何威胁您的数据安全
  • .bat批处理(二):%0 %1——给批处理脚本传递参数
  • .desktop 桌面快捷_Linux桌面环境那么多,这几款优秀的任你选