前言
在使用协程时,不管是看协程的源码还是日常使用,会经常看到 CoroutineScope 和 CoroutineContext, 这两个到底是什么东西呢?作用是什么?
本篇文章我们就来深入的理解一下 CoroutineScope 和 CoroutineContext。
CoroutineScope & CoroutineContext
顾名思义,CoroutineScope 是协程作用域,而 CoroutineContext 则是协程上下文。
听起来比较抽象,他们之间的关系是什么呢?以及它们分别有什么作用呢?
CoroutineScope
先来看 CoroutineScope,它本身是一个接口,内部只有一个CoroutineContext类型的属性 coroutineContext。

另外, 官方基于 CoroutineScope 提供了一些 api 如图所示

在使用协程时,通常我们需要先创建一个协程作用域或者使用提供好的协程作用域,然后再通过协程作用域来创建协程。
代码示例:
fun main() {//创建一个协程作用域val scope = CoroutineScope(Dispatchers.IO)//创建一个协程val job = scope.launch {println("协程执行")}//等待协程结束job.join()
}
上面代码中的 CoroutineScope() 是一个顶层函数,可以传递 CoroutineContext 类型的参数, 最终的实现是由 ContextScope来完成的,此时,会对传递的 CoroutineContext 进行处理,特别是检查其中是否包含 Job 对象,如果没有,则会创建一个新的 Job。
这个Job在之前的协程的结构化这篇文章中也详细讲过,它是协程的父子关系的关键点。

ContextScope 的实现如下:

就是把传入的 CoroutineContext 保存到 coroutineContext 属性中。至此,一个 CoroutineScope 对象也就是协程作用域就创建完成了。
另外,我们可以看一下基于 CoroutineScope 提供的一些其它扩展方法,例如 isActive,cancel 等,这些方法在日常开发中都非常常用。
cancel 方法:

ensureActive 方法:

isActive 方法:

细心的大佬可能会发现,这些方法实际上都依赖于 coroutineContext,在实际的开发中,我们也经常会在协程代码块中通过 coroutineContext 来获取一些协程的上下文信息来做一些操作。
由此可见,CoroutineScope 的核心作用有以下两点:
- 用于 创建协程:如提供了
launch,async等api - 是保存 CoroutineContext 的容器,使开发者能够通过
CoroutineScope来获取CoroutineContext使用。
CoroutineContext
使用场景
CoroutineContext 也是一个接口,上面也说到了它是 CoroutineScope 中的一个属性,是协程上下文信息的载体。
实际上在我们之前使用协程的过程中或多或少都会接触到 CoroutineContext,例如创建协程的时候传递的 CoroutineContext
参数,或者在协程代码块中通过 coroutineContext 获取协程的上下文信息。
例如:创建协程时传递的 CoroutineContext 参数:
CoroutineScope(EmptyCoroutineContext)CoroutineScope(Dispatchers.IO + CoroutineName("IO"))CoroutineScope(Dispatchers.Default + SupervisorJob() + CoroutineExceptionHandler { coroutineContext, throwable ->println("CoroutineExceptionHandler:$throwable")})
在协程中通过coroutineContext 获取协程的上下文信息:
CoroutineScope(EmptyCoroutineContext).launch {val job = coroutineContext.get(Job)val coroutineName = coroutineContext.get(CoroutineName)val continuationInterceptor = coroutineContext.get(ContinuationInterceptor)}
在之前的文章中,我们是直接这么用了,但是你有没有思考过:
Dispatchers.IO + CoroutineName("IO")中+号是什么意思,这样执行后的结果是什么。Job,CoroutineName,ContinuationInterceptor等这些东西是怎么来的,为什么可以通过coroutineContext.get来获取。
先来看一段代码:
fun main() = runBlocking {val context1 = CoroutineName("IO")val context2 = Dispatchers.IOval context3 = SupervisorJob()val newContext = context1 + context2 + context3println(newContext)println("newContext.javaClass:${newContext.javaClass}")val job = newContext.get(Job)println("job:${job}")
}
执行结果:

可以看到
- 通过
+号可以把CoroutineName和Dispatchers.IO合并成一个[CoroutineName(IO), Dispatchers.IO],合并后的类型是CombinedContext类型。 - 通过
get方法能够获取到指定的数据,例如Job对象。
那么,+ 号是怎么实现的呢?get 方法为什么传递的是一个类型参数呢?
在 IDE 中点击+号,会跳转到 CoroutineContext.plus 方法,下面,先来分析下 CoroutineContext 的源码。
源码分析
先来看下 CoroutineContext 类的结构:

可以看到有我们比较常用的 plus,get方法。另外还有一些接口,例如 Element,Key

Element是CoroutineContext子接口,内部重写了get,fold,minusKey等方法。Key接收一个继承自Element的泛型参数。通过注释可以看出,Key是用于标识Element的。
这两个接口在后面会讲到。
plus

plus
plus是被operator修饰的,也就是说它是一个运算符重载函数,可以通过+号来调用。这也就是为什么我们之前可以通过Dispatchers.IO + CoroutineName("IO")这种方式来写代码。- 接收一个
CoroutineContext类型的参数,内部的主要逻辑主要是将传入的context合并到当前CoroutineContext
中,然后返回。如果实现了合并,实际上最终返回一个CombinedContext类型的对象,这点通过plus内部的实现逻辑以及上面运行结果也能看出来。
内部逻辑详解
由于内部逻辑相对比较复杂,这里直接把源码贴过来加注释,方便大家理解。
/*** Plus 合并两个CoroutineContext* 调用者是一个CoroutineContext:其实就是代码块中的this,也就是 + 号左边的* 参数是一个CoroutineContext:对应的是context,也就是 + 号右边的* @param context* @return*/
public operator fun plus(context: CoroutineContext): CoroutineContext =// 如果 + 号右边的是EmptyCoroutineContext,就没有必要继续走合并逻辑了,直接返回 + 号左边的 CoroutineContext 也就是this即可if (context === EmptyCoroutineContext) this else// 如果 + 号右边的不是EmptyCoroutineContext,就需要合并两个 CoroutineContext 了,通过fold函数来累加合并,初始值是this也就是+号左边的值,acc是每次遍历累加的结果,element是每次遍历的元素context.fold(this) { acc, element ->// 先尝试从acc中移除key为element.key的元素, 例如 Job()+Job(),那么就会把acc中把Job移除,此时,acc就变成了EmptyCoroutineContext。如果是 Job()+CoroutineName("IO"),那么就会尝试把 CoroutineName 移除,因为没有CoroutineName可以被移除,此时,acc还是Jobval removed = acc.minusKey(element.key)// 如果移除后的结果是EmptyCoroutineContext,如果是,说明是两个相同的CoroutineContext相加,那么直接返回element即可,没必要继续合并了if (removed === EmptyCoroutineContext) element else {// make sure interceptor is always last in the context (and thus is fast to get when present)val interceptor = removed[ContinuationInterceptor] // 从经过移除后的acc中获取ContinuationInterceptor//如果获取的ContinuationInterceptor是null,说明 removed 中没有 ContinuationInterceptor,那么直接返回创建的CombinedContext(removed, element)if (interceptor == null) CombinedContext(removed, element)else {// 如果获取的ContinuationInterceptor不是null,说明removed中有 ContinuationInterceptor,那么就先把把ContinuationInterceptor移除val left = removed.minusKey(ContinuationInterceptor)//如果移除后发现变成了 EmptyCoroutineContext,例如:EmptyCoroutineContext+Dispatchers.IO 的情况。此时,创建一个CombinedContext(element, interceptor)直接返回if (left === EmptyCoroutineContext)CombinedContext(element, interceptor)else/** 如果移除后不是EmptyCoroutineContext,说明除了ContinuationInterceptor之外还有其他的元素,那么就创建一个CombinedContext(CombinedContext(left, element), interceptor)返回* 目的是先把 ContinuationInterceptor 之外的元进行合并,然后再跟ContinuationInterceptor做合并,以保证ContinuationInterceptor永远在最外层。* */CombinedContext(CombinedContext(left, element),interceptor)}}}
能够看到,plus 方法内部做了一些小优化
- 例如,如果传入的
context是EmptyCoroutineContext,则直接返回无需合并 - 如果在合并的过程中发现有
ContinuationInterceptor,则将ContinuationInterceptor
移动到最外层。这是因为ContinuationInterceptor
是一个非常常用的元素,为了提高获取的效率,将其放在最外层。至于为什么放到外层获取效率就会高,后面在分析CombinedContext
的时候会讲到。
CombinedContext
在分析了 plus 的逻辑后,我们会发现,如果经过了合并操作,最终返回的是 CombinedContext 类型的对象。

下面来看下 CombinedContext 的实现,部分源码如下:

CombinedContext 是 CoroutineContext 的子类,其中包含两个属性 left 和 element。
left: 是CoroutineContext类型。其实就是+号左边的CoroutineContext对象。实际上在经过plus合并后,left的类型就变成CombinedContext类型了。element: 是Element类型。其实就是+号右边的CoroutineContext对象。也就是说,我们+号右边的对象实际上是被限定为Element类型。Element上面也说了,是CoroutineContext的子类。
实际上,经过多次合并后,最终的 CoroutineContext 就变成了类似下面这种嵌套的形式了:
CoroutineContext(CoroutineContext(CoroutineContext(CoroutineContext(),Element)),Element)
这里我们通过一段代码来看吧:
fun main() = runBlocking {val context1 = CoroutineName("IO")println("context1:${context1},context1.javaClass:${context1.javaClass}")val context2 = Dispatchers.IOprintln("context2:${context2},context2.javaClass:${context2.javaClass}")val context3 = SupervisorJob()println("context3:${context3},context3.javaClass:${context3.javaClass}")val newContext1 = context1 + context2println("newContext1:${newContext1},newContext1.javaClass:${newContext1.javaClass}")val newContext2 = context2 + context3println("newContext2:${newContext2},newContext2.javaClass:${newContext2.javaClass}")val newContext3 = newContext1 + newContext2println("newContext3=${newContext3},newContext3.javaClass:${newContext3.javaClass}")val job = newContext2.get(Job)println("job:${job}")
}
这里我们就不看执行结果了,我们来看下断点的信息

可以看到,进过合并操作后,类型就变成了 CombinedContext,而且经过多次合并后,最终就变成了嵌套式的 CombinedContext,看 newContext3 的值也能看出来。
本质上我们可以把 CombinedContext 看作是一个链表的一种结构,因为,他具备 链表的 特征,通过 left 链接节点。
图示如下:
CombinedContext├── left: CombinedContext│ ├── left: CoroutineName("IO")│ └── element: SupervisorJob{Active}└── element: Dispatchers.IO
而上面我们分析的 ContinuationInterceptor 的优化代码也能体现出来,因为,Dispatchers.IO 被移动到了最外层。
再来看一下 CombinedContext 中的 get 方法:
override fun <E : Element> get(key: Key<E>): E? {var cur = this //当前的 CombinedContextwhile (true) {cur.element[key]?.let { return it } //如果当前 CombinedContext 的element是key 对应的元素不为空,直接返回val next = cur.left//获取当前 CombinedContext 的leftif (next is CombinedContext) { //如果left是 CombinedContextcur = next //把next赋值给cur,继续循环} else {return next[key] //如果next不是 CombinedContext,直接返回next的key对应的元素}}
}
可以看到,get 方法实际上就是优先从 element 开始获取,找不到的话再从 left 继续找。
这也是说面说到的 ContinuationInterceptor 放在最外层以便于提高获取效率。
我们再来回忆一下,我们获取 指定元素时,传递的是具体的类型,如下:
val job = newContext3.get(Job)
那么问题来了,这是如何通过类型找到的对应的实例的呢?还记得一开始我们提到的 Key<E> 这个接口吗?
Key 中的 泛型 实际上是被限定为 Element 类型了。

而 Element 接口中,又具备 key:Key<*> 属性,同时也提供了 get 方法用于匹配并返回指定的泛型类型给到调用处。

还是以 Job 为例吧

可以看到,Job 继承自 Element,内部的 Key 是 Job 的伴生对象,并且是 CoroutineContext.Key<Job>类型的,说白了这个Key就是用于标记 Job 的。
当通过 newContext3.get(Job) 时,实际上调用的是上面说到的 CombinedContext 中的 get方法。
CombinedContext 中的 get 方法 最终会遍历的调用 element 元素的 get(key: Key<E>)
方法直到满足条件返回。
也就是说最终 Job.get(Key<Job>) 会满足条件,然后满足条件的 Job 就会被返回了。
除了Job,其他能够被获取的元素例如
ContinuationInterceptor
ContinuationInterceptor 用于指定协程的代码块在哪个线程执行,例如我们常用的Dispatchers.IO,Dispatchers.Main 都是ContinuationInterceptor 的实现。
以 Dispatchers.IO 为例,可以看到 Dispatchers.IO 实际是 CoroutineDispatcher 类型的。

CoroutineDispatcher 又实现了 ContinuationInterceptor 接口。

ContinuationInterceptor 内部则有 Key 用来标识 ContinuationInterceptor 。

CoroutineName
CoroutineName 比较简单,一般用于表示标识协程的名称,以便于我们区分。

内部也有 Key 用来标识 CoroutineName。
到这里,我们之前常用的CoroutineContext 的 + 以及 get 方法就搞清楚了。
自定义 CoroutineContext
除了内置的 CoroutineContext,我们还可以自定义 CoroutineContext。
自定义 CoroutineContext 可以给协程提供更加丰富的上下文信息,例如我希望在给协程加一个请求ID,以便于在做日志上报的时候能够区分每个请求。
代码示例:
/*** 自定义CoroutineContext* 关键点在于继承自AbstractCoroutineContextElement,并且提供 companion object Key* @property requestId* @constructor Create empty Log context*/
class LogContext(val requestId: String) : AbstractCoroutineContextElement(LogContext) {companion object Key : CoroutineContext.Key<LogContext>
}//给个扩展方法用于获取requestId
val CoroutineContext.requestId: Stringget() = this[LogContext]?.requestId ?: ""fun main(): Unit = runBlocking {val logContext = LogContext("req_11111")// 在协程中使用日志上下文launch(logContext) {println("Processing request with ID: ${coroutineContext.requestId}")// 这里执行与请求相关的逻辑}
}
执行结果:

日常开发中,自定义 CoroutineContext的场景并不多见,但是还是要知道有这个东西,在需要的时候可以想起来用它。
总结
基本上我们已经把 CoroutineScope 和 CoroutineContext 的核心内容都讲完了,这两个东西在协程中是非常重要的,下面来总结一下:
CoroutineScope
- CoroutineScope 是协程作用域,它的核心作用是用于创建协程以及保存
CoroutineContext。
CoroutineContext
CoroutineContext是协程上下文,存储了协程的上下文信息,例如Job,CoroutineName,ContinuationInterceptor等。CoroutineContext中的+号是运算符重载函数,用于合并两个CoroutineContext。- 合并后的
CoroutineContext是CombinedContext类型,本质上是一个链表的结构。
Key
Key<E>是一个带泛型的接口,用于标识Element。- 可以通过
Key来获取指定的Element。
Element
Element是CoroutineContext的子接口,内部重写了 get,fold,minusKey 等方法,同时具备 key 属性。- 所有继承自
Element的类都会提供一个Key伴生对象,用于标识Element,在获取指定元素时,通过 Key 来匹配。
CombinedContext
CombinedContext是CoroutineContext的子类,用于保存合并后的CoroutineContext。CombinedContext是一个链表的结构,通过 left 属性链接节点。CombinedContext中的 get 方法是优先从 element 中获取,找不到的话再从 left 中继续找。CombinedContext中的 get 方法最终调用的是Element中的 get 方法,通过 Key 匹配来获取指定的 Element。
CombinedContext 中经常会获取的元素
JobCoroutineNameContinuationInterceptor
自定义 CoroutineContext
- 继承自
AbstractCoroutineContextElement - 提供
Key用于标识Element
好了,本篇文章就是这样,希望能够对你有所帮助。
感谢阅读,如果对你有帮助请三连(点赞、收藏、加关注)支持。有任何疑问或建议,欢迎在评论区留言讨论。如需转载,请注明出处:喻志强的博客
