Kotlin协程详解——协程上下文
目录
一、上下文结构
get()获取元素
minusKey()删除元素
fold()元素遍历
plus()添加元素
CombinedContext
Key
二、协程名称CoroutineName
三、上下文组合
四、协程作用域CoroutineScope
五、典型用例
协程的上下文,它包含用户定义的一些数据集合,这些数据与协程密切相关。它类似于map集合,可以通过key来获取不同类型的数据。同时CoroutineContext的灵活性很强,如果其需要改变只需使用当前的CoroutineContext来创建一个新的CoroutineContext即可。
在协程启动部分提到,启动协程需要三个部分,其中一个部分就是上下文,其接口类型是CoroutineContext,通常所见的上下文类型是CombinedContext或者EmptyCoroutineContext,一个表示上下文组合,另一个表示空。
协程上下文是Kotlin协程的基本结构单元,主要承载着资源获取,配置管理等工作,是执行环境的通用数据资源的统一管理者。除此之外,也包括携带参数,拦截协程执行等,是实现正确的线程行为、生命周期、异常以及调试的关键。
协程使用以下几种元素集定义协程行为,他们均继承自CoroutineContext:
- 【Job】:协程的句柄,对协程的控制和管理生命周期。
- 【CoroutineName】:协程的名称,用于调试
- 【CoroutineDispatcher】:调度器,确定协程在指定的线程执行
- 【CoroutineExceptionHandler】:协程异常处理器,处理未捕获的异常

简而言之,协程上下文是协程必备组成部分,管理了协程的线程绑定、生命周期、异常处理和调试。
一、上下文结构
看一下CoroutineContext的接口定义:

每一个CoroutineContext都有它唯一的一个Key其中的类型是Element,我们可以通过对应的Key来获取对应的具体对象。说的有点抽象我们直接通过例子来了解。
var context = Job() + Dispatchers.IO + CoroutineName("aa")
LogUtils.d("$context, ${context[CoroutineName]}")
context = context.minusKey(Job)
LogUtils.d("$context")
// 输出
[JobImpl{Active}@158b42c, CoroutineName(aa), LimitingDispatcher@aeb0f27[dispatcher = DefaultDispatcher]], CoroutineName(aa)
[CoroutineName(aa), LimitingDispatcher@aeb0f27[dispatcher = DefaultDispatcher]]
Element:协程上下文的一个元素,本身就是一个单例上下文,里面有一个key,是这个元素的索引。
Job、Dispatchers与CoroutineName都实现了Element接口。如果需要结合不同的CoroutineContext可以直接通过+拼接,本质就是使用了plus方法。
可知,Element本身也实现了CoroutineContext接口。
这里我们再看一下官方解释:
/**
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].*/
从官方解释可知,CoroutineContext是一个Element的集合,这种集合被称为indexed set,介于set 和 map 之间的一种结构。set 意味着其中的元素有唯一性,map 意味着每个元素都对应一个键。
如果将协程上下文内部的一系列上下文称为子上下文,上下文为每个子上下文分配了一个Key,它是一个带有类型信息的接口。
这个接口通常被实现为companion object。

源码中定义的子上下文,都会在内部声明一个静态的Key,类内部的静态变量意味着被所有类实例共享,即全局唯一的 Key 实例可以对应多个子上下文实例。
在一个类似 map 的结构中,每个键必须是唯一的,因为对相同的键 put 两次值,新值会代替旧值。通过上述方式,通过键的唯一性保证了上下文中的所有子上下文实例都是唯一的。
我们按照这个格式仿写一下然后反编译。

对比kt和Java文件,可以看到Key就是一个静态变量,且其实现类未做处理,作用与HashMap中的Key类似。
Key是静态变量,全局唯一,为Element提供唯一性保障。
前述内容总结如下:
- 协程上下文是一个元素的集合,单个元素本身也是一个上下文,其定义是递归的,自己包含若干个自己。
- 协程上下文这个集合有点像 set 结构,其中的元素都是唯一的,不重复的。其通过给每一个元素配有一个静态的键实例,构成一组键值对的方式实现。这使其类似 map 结构。这种介于 set 和 map 之间的结构称为indexed set。
get()获取元素
关于CoroutineContext,我们先看一下其是如何取元素的。
这里看一下Element、CombinedContext、EmptyCoroutineContext的内部实现,其中CombinedContext就是CoroutineContext集合结构的实现,EmptyCoroutineContext就表示一个空的CoroutineContext,它里面是空实现。

通过Key检索Element,返回值只能是Element或null,链表节点中的元素值,其中CombinedContext利用while循环实现了类似递归的效果,其中较早被遍历到的元素自然具有较高的优先级。

minusKey()删除元素
同理看一下Element、CombinedContext、EmptyCoroutineContext的内部实现。
internal class CombinedContext(//左上下文private val left: CoroutineContext,//右元素private val element: Element
) : CoroutineContext, Serializable {public override fun minusKey(key: Key<*>): CoroutineContext {//如果element就是要删除的元素,返回left,否则说明要删除的元素在left中,继续从left中删除对应的元素element[key]?.let { return left }//在左上下文中去掉对应元素val newLeft = left.minusKey(key)return when {//如果left中不存在要删除的元素,那么当前CombinedContext就不存在要删除的元素,直接返回当前CombinedContext实例newLeft === left -> this//如果left中存在要删除的元素,删除了这个元素后,left变为了空,那么直接返回当前CombinedContext的element就行newLeft === EmptyCoroutineContext -> element//如果left中存在要删除的元素,删除了这个元素后,left不为空,那么组合一个新的CombinedContext返回else -> CombinedContext(newLeft, element)}}......
}public object EmptyCoroutineContext : CoroutineContext, Serializable {public override fun minusKey(key: Key<*>): CoroutineContext = this......
}public interface Element : CoroutineContext {//如果key和自己的key匹配,那么自己就是要删除的Element,返回EmptyCoroutineContext(表示删除了自己),否则说明自己不需要被删除,返回自己public override fun minusKey(key: Key<*>): CoroutineContext =if (this.key == key) EmptyCoroutineContext else this......
}
如果把CombinedContext和Element结合来看,那么CombinedContext的整体结构如下:

其结构类似链表,left就是指向下一个结点的指针,get、minusKey操作大体逻辑都是先访问当前element,不满足,再访问left的element,顺序都是从right到left。
fold()元素遍历

fold也是递归的形式操作,fold的操作大体逻辑是:先访问left,直到递归到最后的element,然后再从left到right的返回,从而访问了所有的element。
plus()添加元素
关于CoroutineContext的元素添加方法,直接看其plus()实现,也是唯一没有被重写的方法。
public operator fun plus(context: CoroutineContext): CoroutineContext =
//如果要相加的CoroutineContext为空,那么不做任何处理,直接返回
if (context === EmptyCoroutineContext) this else
//如果要相加的CoroutineContext不为空,那么对它进行fold操作,可以把acc理解成+号左边的CoroutineContext,element理解成+号右边的CoroutineContext的某一个element
context.fold(this) { acc, element ->//首先从左边CoroutineContext中删除右边的这个elementval removed = acc.minusKey(element.key)//如果removed为空,说明左边CoroutineContext删除了和element相同的元素后为空,那么返回右边的element即可if (removed === EmptyCoroutineContext) element else {//如果removed不为空,说明左边CoroutineContext删除了和element相同的元素后还有其他元素,那么构造一个新的CombinedContext返回val interceptor = removed[ContinuationInterceptor]if (interceptor == null) CombinedContext(removed, element) else {val left = removed.minusKey(ContinuationInterceptor)if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) elseCombinedContext(CombinedContext(left, element), interceptor)}}}
plus方法大部分情况下返回一个CombinedContext,即我们把两个CoroutineContext相加后,返回一个CombinedContext,在组合成CombinedContext时,+号右边的CoroutineContext中的元素会覆盖+号左边的CoroutineContext中的含有相同key的元素。plus的实现逻辑是将两个拼接的CoroutineContext封装到CombinedContext中组成一个拼接链,同时每次都将ContinuationInterceptor添加到拼接链的最尾部.
这个覆盖操作就在fold方法的参数operation代码块中完成,通过minusKey方法删除掉重复元素。
plus方法中可以看到里面有个对ContinuationInterceptor的处理,目的是让ContinuationInterceptor在每次相加后都能变成CoroutineContext中的最后一个元素。
ContinuationInterceptor继承自Element,称为协程上下文拦截器,作用是在协程执行前拦截它,从而在协程执行前做出一些其他的操作。通过把ContinuationInterceptor放在最后面,协程在查找上下文的element时,总能最快找到拦截器,避免了递归查找,从而让拦截行为前置执行。
CombinedContext
internal class CombinedContext(private val left: CoroutineContext,private val element: Element
) : CoroutineContext, Serializable {override fun <E : Element> get(key: Key<E>): E? {var cur = thiswhile (true) {cur.element[key]?.let { return it }val next = cur.leftif (next is CombinedContext) {cur = next} else {return next[key]}}}...
}
注意看它的两个参数,我们直接拿上面的例子来分析
Job() + Dispatchers.IO
(Job, Dispatchers.IO)
Job对应于left,Dispatchers.IO对应element。如果再拼接一层CoroutineName(aa)就是这样的
((Job, Dispatchers.IO),CoroutineName)
功能类似与链表,但不同的是你能够拿到上一个与你相连的整体内容。与之对应的就是minusKey方法,从集合中移除对应Key的CoroutineContext实例。
有了这个基础,我们再看它的get方法就很清晰了。先从element中去取,没有再从之前的left中取。
Key
那么这个Key到底是什么呢?我们来看下CoroutineName
public data class CoroutineName(/*** User-defined coroutine name.*/val name: String
) : AbstractCoroutineContextElement(CoroutineName) {/*** Key for [CoroutineName] instance in the coroutine context.*/public companion object Key : CoroutineContext.Key<CoroutineName>/*** Returns a string representation of the object.*/override fun toString(): String = "CoroutineName($name)"
}
很简单它的Key就是CoroutineContext.Key<CoroutineName>,当然这样还不够,需要继续结合对于的operator get方法,所以我们再来看下Element的get方法
public override operator fun <E : Element> get(key: Key<E>): E? =@Suppress("UNCHECKED_CAST")if (this.key == key) this as E else null
这里使用到了Kotlin的operator操作符重载的特性。那么下面的代码就是等效的。
context.get(CoroutineName)
context[CoroutineName]
所以我们就可以直接通过类似于Map的方式来获取整个协程中CoroutineContext集合中对应Key的CoroutineContext实例。
二、协程名称CoroutineName

CoroutineName是用户用来指定的协程名称的,用于方便调试和定位问题。

协程内部可以通过coroutineContext这个全局属性直接获取当前协程的上下文。
三、上下文组合
如果要传递多个上下文元素,CoroutineContext可以使用"+"运算符进行合并。由于CoroutineContext是由一组元素组成的,所以加号右侧的元素会覆盖加号左侧的元素,进而组成新创建的CoroutineContext。

如果有重复的元素(key一致)则右边的会代替左边的元素,相关原理参看协程上下文结构章节。
四、协程作用域CoroutineScope
CoroutineScope实际上是一个CoroutineContext的封装,当我们需要启动一个协程时,会在CoroutineScope的实例上调用构建函数,如async和launch。
在构建函数中,一共出现了3个CoroutineContext。

查看协程构建函数async和launch的源码,其第一行都是如下代码:

进一步查看:

构建器内部进行了一个CoroutineContext拼接操作,plus左值是CoroutineScope内部的CoroutineContext,右值是作为构建函数参数的CoroutineContext。
抽象类AbstractCoroutineScope实现了CoroutineScope和Job接口。大部分CoroutineScope的实现都继承自AbstractCoroutineScope,意味着他们同时也是一个Job。

从上述分析可知:coroutine context = parent context + coroutine job
五、典型用例
全限定Context
launch( Dispatchers.Main + Job() + CoroutineName("HelloCoroutine") + CoroutineExceptionHandler { _, _ -> /* ... */ }) {
/* ... */
}
全限定Context,即全部显式指定具体值的Elements。不论你用哪一个CoroutineScope构建该协程,它都具有一致的表现,不会受到CoroutineScope任何影响。
CoroutineScope Context
基于Activity生命周期实现一个CoroutineScope

Dispatcher:使用Dispatcher.Main,以在UI线程进行绘制
Job:在onCreate时构建,在onDestroy时销毁,所有基于该CoroutineContext创建的协程,都会在Activity销毁时取消,从而避免Activity泄露的问题
临时指定参数
CoroutineContext的参数主要有两个来源:从scope中继承+参数指定。我们可以用withContext便捷地指定某个参数启动子协程,例如我们想要在协程内部执行一个无法被取消的子协程:

读取协程上下文参数
通过顶级挂起只读属性coroutineContext获取协程上下文参数,它位于 kotlin-stdlib / kotlin.coroutines / coroutineContext

Nested Context内嵌上下文
内嵌上下文切换:在协程A内部构建协程B时,B会自动继承A的Dispatcher。
可以在调用async时加入Dispatcher参数,切换到工作线程

推荐文章
https://zhuanlan.zhihu.com/p/552225674
Kotlin协程实现原理:Suspend&CoroutineContext
相关文章:
Kotlin协程详解——协程上下文
目录 一、上下文结构 get()获取元素 minusKey()删除元素 fold()元素遍历 plus()添加元素 CombinedContext Key 二、协程名称CoroutineName 三、上下文组合 四、协程作用域CoroutineScope 五、典型用例 协程的上下文,它包含用户定义的一些数据集合&#x…...
手写一个C++ Android Binder服务及源码分析
手写一个C Android Binder服务及源码分析 前言一、 基于C语言编写Android Binder跨进程通信Demo总结及改进二、C语言编写自己的Binder服务Demo1. binder服务demo功能介绍2. binder服务demo代码结构图3. binder服务demo代码实现3.1 IHelloService.h代码实现3.2 BnHelloService.c…...
今日AI和商界事件(2025-02-10)
今日AI领域的相关事件包括: 一、技术与应用进展 全球首例AI驱动供应链攻击曝光: 网络安全机构披露一起新型供应链攻击事件,攻击者利用AI技术生成高度仿真的供应商邮件,诱骗目标企业员工下载恶意软件,进而渗透至大众汽…...
全面理解-c++中的异常处理机制
C 的异常处理机制是一种用于处理程序运行时错误的结构化方法,通过分离正常逻辑与错误处理代码,提高代码的可读性和可维护性。以下是其核心组成部分和工作原理的详细说明: 1. 异常处理的三大关键字 1.1 try 块 作用:包裹可能抛出异…...
Deep Dive into LLMs like ChatGPT - by Andrej Karpathy
https://www.youtube.com/watch?v7xTGNNLPyMIhttps://www.youtube.com/watch?v7xTGNNLPyMIDeep Dive into LLMs like ChatGPT - by Andrej Karpathy_哔哩哔哩_bilibilihttps://www.youtube.com/watch?v7xTGNNLPyMI转载自Andrej Karpathy Youtube ChannelThis is a general a…...
react实例与总结(一)
目录 一、简单认识 1.1、特点 1.2、JSX语法规则 1.3、函数组件和类式组件 1.4、类组件三大属性state、props、refs 1.4.1、state 1.4.2、props 1.4.3、refs 1.5、事件处理 1.6、收集表单数据—非受控组件和受控组件 1.7、高阶函数—函数柯里化 1.8、生命周期—新旧…...
51单片机(国信长天)矩阵键盘的基本操作
在CT107D单片机综合训练平台上,首先将J5处的跳帽接到1~2引脚,使按键S4~S19按键组成4X4的矩阵键盘。在扫描按键的过程中,发现有按键触发信号后(不做去抖动),待按键松开后,在数码管的第一位显示相应的数字:从左至右&…...
在cursor/vscode中使用godot C#进行游戏开发
要在 Visual Studio Code(VS Code)中启动 C#Godot 项目,可以按照以下步骤进行配置: 1.安装必要的工具 • 安装 Visual Studio Code:确保你已经安装了最新版本的 VS Code。 • 安装.NET SDK:下载并安装.NET 7.x SDK(…...
机器学习怎么学习,还有算法基本的源代码
1.scikit-learn官方文档,中文版/英文版 中文社区:https://scikit-learn.org.cn/ 中文官方文档:https://scikitlearn.com.cn/ 英文版:https://scikit-learn.org/stable/(翻墙) 2.菜鸟教程:AI&a…...
STM32 RTC亚秒
rtc时钟功能实现:rtc模块在stm32内部,由电池或者主电源供电。如下图,需注意实现时仅需设置一次初始化。 1、stm32cubemx 代码生成界面设置,仅需开启时钟源和激活日历功能。 2、生成的代码,需要对时钟进行初始化,仅需…...
【Linux】深入理解linux权限
🌟🌟作者主页:ephemerals__ 🌟🌟所属专栏:Linux 目录 前言 一、权限是什么 二、用户和身份角色 三、文件属性 1. 文件属性表示 2. 文件类型 3. 文件的权限属性 四、修改文件的权限属性和角色 1. …...
json格式,curl命令,及轻量化处理工具
一. JSON格式 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。它基于一个子集的JavaScript编程语言,使用人类易于阅读的文本格式来存储和表示数据。尽管名字中有“JavaScript”,但JSON是语言无关的,几…...
DeepSeek模拟阿里面试——java面向对象
作为一位阿里高级Java程序员面试官,我会围绕Java面向对象编程的核心概念、实际应用以及设计原则设计问题,以全面评估候选人的理解和应用能力。以下是可能的面试问题: 基本概念与实现方式 请解释Java中封装、继承、多态的基本概念及其在Java中…...
web直播弹幕抓取分析 signature
声明: 本文章中所有内容仅供学习交流使用,不用于其他任何目的,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关! 前言 最近遇到太多难点了卡了很久&am…...
【04】RUST特性
文章目录 隐藏shadowing所有权ownership堆区&栈区所有权规则变量&数据Copy Trait与Drop TraitCopy TraitDrop Trait移动克隆函数参数与返回值的所有权参数引用可变引用悬垂引用slice生命周期隐藏shadowing 有点像同名覆盖 let mut guess = String::new();let guess: u3…...
PL/SQL块结构
目录 一、声明部分(declare) 二、执行部分(begin end) 三、异常处理部分 (Exception end) 四、代码示例 PL/SQL(Procedural Language/Structured Query Language)是Oracle数据库…...
基于 FFmpeg 和 OpenGLES 的 iOS 视频预览和录制技术方案设计
基于 FFmpeg 和 OpenGLES 的 iOS 视频预览和录制技术方案设计 在 iOS 上实现一个基于 FFmpeg 和 OpenGLES 的视频预览和录制功能,需要结合 FFmpeg 的强大音视频处理能力和 OpenGLES 的高效图形渲染能力。以下是一个完整的技术方案设计,包含项目的架构设计、模块划分、技术选…...
【LeetCode 刷题】贪心算法(4)-区间问题
此博客为《代码随想录》贪心算法章节的学习笔记,主要内容为贪心算法区间问题的相关题目解析。 文章目录 55. 跳跃游戏45. 跳跃游戏 II452. 用最少数量的箭引爆气球435. 无重叠区间763. 划分字母区间56. 合并区间 55. 跳跃游戏 题目链接 class Solution:def canJu…...
提示工程 | 目的 | 常用技巧
什么是提示工程 提示工程也叫指令工程,Prompt就是你发给大模型的指令,比如:画幅画,写首诗等。貌似简单,但意义非凡,Prompt是AGI时代的编程语言,Prompt工程是AGI时代的软件工程,提示…...
ABP框架9——自定义拦截器的实现与使用
一、AOP编程 AOP定义:面向切片编程,着重强调功能,将功能从业务逻辑分离出来。AOP使用场景:处理通用的、与业务逻辑无关的功能(如日志记录、性能监控、事务管理等)拦截器:拦截方法调用并添加额外的行为,比如…...
Generate html
"Generate HTML"(生成 HTML)指的是通过程序或工具自动创建 HTML 代码的过程。HTML(超文本标记语言)是用于创建网页内容和结构的标准语言。生成 HTML 通常意味着通过某些方式自动化地构建或生成网页的结构和元素…...
CUDA 计算平台 CUDA 兼容性【笔记】
在 b 站看过的两个关于 CUDA 的技术分享,整理分享下对自己有用的课件。 20231130 2023第9期 聊一聊常见的AI计算平台库_哔哩哔哩_bilibili20230831 2023第6期 聊一聊CUDA兼容性_哔哩哔哩_bilibili 文章目录 CUDA 计算平台CUDA 函数库介绍英伟达三大护城河࿱…...
移动(新)魔百盒刷机教程[M301A_YS]
刚刚成功刷了一个坏的魔百盒,简单记录一下。 刷电视盒子有两种:卡刷和线刷。 线刷 一、线刷准备 1.刷机工具 Amlogic USB Burning Tool 晶晨线刷烧录工具 2.固件 根据盒子的型号、代工等找到对应的固件 二、线刷步骤 电脑打开下好的 Amlogic US…...
最新消息 | 德思特荣获中国创新创业大赛暨广州科技创新创业大赛三等奖!
2024年12月30日,广州市科技局公开第十三届中国创新创业大赛(广东广州赛区)暨2024年广州科技创新创业大赛决赛成绩及拟获奖企业名单,德思特获得了智能与新能源汽车初创组【第六名】【三等奖】的好成绩! 关于德思特&…...
基于机器学习的DDoS检测系统实战
基于机器学习的DDoS检测系统实战(PythonScikit-learn)|毕业设计必备 摘要:本文手把手教你从0到1实现一个轻量级DDoS攻击检测系统,涵盖数据预处理、特征工程、模型训练与可视化分析。 一、项目背景与意义 DDoS&#x…...
ubuntu安装VMware报错/dev/vmmon加载失败
ubuntu安装VMware报错/dev/vmmon加载失败,解决步骤如下: step1:为vmmon和vmnet组件生成密钥对 openssl req -new -x509 -newkey rsa:2048 -keyout VMW.priv -outform DER -out VMW.der -nodes -days 36500 -subj "/CNVMware/"ste…...
使用条件随机场(CRF)进行文本分类并评估模型性能
目标: 使用条件随机场(CRF)模型对文本数据进行分类,并评估模型的性能。任务包括读取数据、划分训练集和测试集、训练CR # 1.数据读取与预处理: # o使用open函数读取包含文本和标签的CSV文件。 # o将每一行数据分为文本…...
python的列表、元组、深拷贝、浅拷贝(四)
python的列表 一、序列1. 序列定义2. 序列数据类型包括3.特点:都支持下面的特性 二、 列表1. 列表的创建2. 列表的基本特性(1) 连接操作符喝重复操作符(2) 成员操作符(in , not in )(3) 索引(4) 切片练习(5) for循环 3. 列表的常用方法(1) 一…...
2.10作业
思维导图 C C语言...
【深度学习】多目标融合算法(四):多门混合专家网络MMOE(Multi-gate Mixture-of-Experts)
目录 一、引言 二、MMoE(Multi-gate Mixture-of-Experts,多门混合专家网络) 2.1 技术原理 2.2 技术优缺点 2.3 业务代码实践 2.3.1 业务场景与建模 2.3.2 模型代码实现 2.3.3 模型训练与推理测试 2.3.4 打印模型结构 三、总结 一、…...
