前言
在使用协程时,不管是看协程的源码还是日常使用,会经常看到 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 中经常会获取的元素
Job
CoroutineName
ContinuationInterceptor
自定义 CoroutineContext
- 继承自
AbstractCoroutineContextElement
- 提供
Key
用于标识Element
好了,本篇文章就是这样,希望能够对你有所帮助。
感谢阅读,如果对你有帮助请三连(点赞、收藏、加关注)支持。有任何疑问或建议,欢迎在评论区留言讨论。如需转载,请注明出处:喻志强的博客