欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 健康 > 养生 > kotlin协程之CoroutineScope 与 CoroutineContext 详解

kotlin协程之CoroutineScope 与 CoroutineContext 详解

2025/3/9 16:35:15 来源:https://blog.csdn.net/yuzhiqiang_1993/article/details/146118925  浏览:    关键词:kotlin协程之CoroutineScope 与 CoroutineContext 详解

前言

在使用协程时,不管是看协程的源码还是日常使用,会经常看到 CoroutineScopeCoroutineContext, 这两个到底是什么东西呢?作用是什么?
本篇文章我们就来深入的理解一下 CoroutineScopeCoroutineContext

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 提供的一些其它扩展方法,例如 isActivecancel 等,这些方法在日常开发中都非常常用。

cancel 方法:
在这里插入图片描述

ensureActive 方法:
在这里插入图片描述

isActive 方法:
在这里插入图片描述

细心的大佬可能会发现,这些方法实际上都依赖于 coroutineContext,在实际的开发中,我们也经常会在协程代码块中通过 coroutineContext 来获取一些协程的上下文信息来做一些操作。

由此可见,CoroutineScope 的核心作用有以下两点:

  1. 用于 创建协程:如提供了 launch,async 等api
  2. 是保存 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")+ 号是什么意思,这样执行后的结果是什么。
  • JobCoroutineNameContinuationInterceptor 等这些东西是怎么来的,为什么可以通过 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}")
}

执行结果:
在这里插入图片描述

可以看到

  • 通过 + 号可以把 CoroutineNameDispatchers.IO 合并成一个 [CoroutineName(IO), Dispatchers.IO],合并后的类型是 CombinedContext 类型。
  • 通过 get 方法能够获取到指定的数据,例如 Job 对象。

那么,+ 号是怎么实现的呢?get 方法为什么传递的是一个类型参数呢?

IDE 中点击+号,会跳转到 CoroutineContext.plus 方法,下面,先来分析下 CoroutineContext 的源码。

源码分析

先来看下 CoroutineContext 类的结构:
在这里插入图片描述

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

在这里插入图片描述

  • ElementCoroutineContext 子接口,内部重写了 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 方法内部做了一些小优化

  • 例如,如果传入的 contextEmptyCoroutineContext,则直接返回无需合并
  • 如果在合并的过程中发现有 ContinuationInterceptor ,则将 ContinuationInterceptor
    移动到最外层。这是因为 ContinuationInterceptor
    是一个非常常用的元素,为了提高获取的效率,将其放在最外层。至于为什么放到外层获取效率就会高,后面在分析 CombinedContext
    的时候会讲到。
CombinedContext

在分析了 plus 的逻辑后,我们会发现,如果经过了合并操作,最终返回的是 CombinedContext 类型的对象。

在这里插入图片描述

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

在这里插入图片描述

CombinedContextCoroutineContext 的子类,其中包含两个属性 leftelement

  • 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.IODispatchers.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的场景并不多见,但是还是要知道有这个东西,在需要的时候可以想起来用它。

总结

基本上我们已经把 CoroutineScopeCoroutineContext 的核心内容都讲完了,这两个东西在协程中是非常重要的,下面来总结一下:

CoroutineScope

  • CoroutineScope 是协程作用域,它的核心作用是用于创建协程以及保存 CoroutineContext

CoroutineContext

  • CoroutineContext 是协程上下文,存储了协程的上下文信息,例如 JobCoroutineNameContinuationInterceptor 等。
  • CoroutineContext 中的 + 号是运算符重载函数,用于合并两个 CoroutineContext
  • 合并后的 CoroutineContextCombinedContext 类型,本质上是一个链表的结构。

Key

  • Key<E> 是一个带泛型的接口,用于标识 Element
  • 可以通过 Key 来获取指定的 Element

Element

  • ElementCoroutineContext 的子接口,内部重写了 get,fold,minusKey 等方法,同时具备 key 属性。
  • 所有继承自 Element 的类都会提供一个 Key 伴生对象,用于标识 Element,在获取指定元素时,通过 Key 来匹配。

CombinedContext

  • CombinedContextCoroutineContext 的子类,用于保存合并后的 CoroutineContext
  • CombinedContext 是一个链表的结构,通过 left 属性链接节点。
  • CombinedContext 中的 get 方法是优先从 element 中获取,找不到的话再从 left 中继续找。
  • CombinedContext 中的 get 方法最终调用的是 Element 中的 get 方法,通过 Key 匹配来获取指定的 Element。

CombinedContext 中经常会获取的元素

  • Job
  • CoroutineName
  • ContinuationInterceptor

自定义 CoroutineContext

  • 继承自 AbstractCoroutineContextElement
  • 提供 Key 用于标识 Element

好了,本篇文章就是这样,希望能够对你有所帮助。


感谢阅读,如果对你有帮助请三连(点赞、收藏、加关注)支持。有任何疑问或建议,欢迎在评论区留言讨论。如需转载,请注明出处:喻志强的博客

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词