Kotlin 协程源码阅读笔记 —— CoroutineContext

Kotlin 协程源码阅读笔记 ------ CoroutineContext

如果你还了解过 Kotlin 协程,我建议你先了解一下 Kotlin 协程再来阅读本篇文章,不然你可能会看不懂本篇文章。

如果我们需要开启一个协程,首先需要构建一个 CoroutineScope 对象,然后通过这个对象再调用 launch() 方法就启动了一个协程,而 CortouineScope 只是一个简单的接口,需要实现类提供 CoroutineContext 对象,哈哈,终于见到我们本篇文章的主题了😂,通常我们会传递一个 CoroutineDispacher 对象作为默认的 CoroutineContext,如果是你需要让协程的方法在主线程调度,就使用 Dispatchers.Main,如果需要协程的方法在非主线程调用,就使用 Dispatchers.Default。当然如果你不需要任何默认的 CoroutineContext,可以指定为 EmptyCoroutineContext。上面提到的 CoroutineDispatcher (Dispatchers.MainDispatchers.Default) 与 EmptyCoroutineContext 他们也都是属于 CoroutineContext,这里简单介绍一下常见的 CoroutineContext

  • EmptyCoroutineContext
    表示空的 CoroutineContext,他没有任何的功能,只是一个标志作用,表示没有 CoroutineContext
  • CoroutineDispatcher
    控制协程运行调度相关的工作,协程的方法在具体的哪个线程执行也由它控制,常见的 CoroutineDispatcherDispatchers.MainDispatchers.Default
  • Job
    表示协程任务的状态,当协程执行时会添加一个 Job,当协程开启一个子协程时又会开启一个 Job,子协程的 Job 就是作为父协程的 Child,反之就是 Parent。当父协程的 Job 被取消时,所有的子协程也都会被取消。我们在构建 CoroutineScope 对象时,系统会为我们添加一个默认 Job,通过该 CoroutineScope 启动的协程的 Job 都是 CoroutineScope 的子 Job,如果想要停止所有的 CoroutineScope 启动的协程时,可以通过调用 CoroutineScope#cancel() 方法,在这个方法里面就是调用的 Jobcancel() 方法。
  • CoroutineExceptionHandler
    协程异常拦截器,我们可以自定义这个拦截器用来拦截协程中遇到的异常,不是有很多人遇到协程中的奇怪的崩溃吗?当遇到某些不需要处理的崩溃时,就可以用它过滤掉。
  • CoroutineId
    用来指定协程的 Id,会通过它来修改执行的线程的名字,这个是系统调用的。

上面介绍了一些系统的 CoroutineContext,这些 CoroutineContext 在协程执行的过程中发挥着重要的作用,你自己也可以实现一些 CoroutineContext

Element 和 CombinedContext

我们上面提到的系统的 CoroutineContext 除了 EmptyCoroutineContext 以外,其他的都是实现了 Element 接口(它继承于 CoroutineContext),Element 表示实现了特定某项功能的 CoroutineContext,而 CombinedContext 就表示一堆 Element 组成的复合型 CoroutineContext,比如通过 JobCoroutineDispatcher 就能够组合成一个 CombinedContext,也就是相当于一个 Element 的集合。

Element

Kotlin 复制代码
    public interface Element : CoroutineContext {
        /**
         * A key of this coroutine context element.
         */
        public val key: Key<*>

        public override operator fun <E : Element> get(key: Key<E>): E? =
            @Suppress("UNCHECKED_CAST")
            if (this.key == key) this as E else null

        public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
            operation(initial, this)

        public override fun minusKey(key: Key<*>): CoroutineContext =
            if (this.key == key) EmptyCoroutineContext else this
    }

Element 中有一个非常重要的对象 Key,它表示这种类型的 KeyElementCombinedContext 中只有一个这种类型的 Element。通常这个 Key 的实现都是一个 Kotlin 中的伴生对象(也就是一个单例对象),我这里以 ExecutorCoroutineDispatcher 中的 Key 来举一个例子:

Kotlin 复制代码
    @ExperimentalStdlibApi
    public companion object Key : AbstractCoroutineContextKey<CoroutineDispatcher, ExecutorCoroutineDispatcher>(
        CoroutineDispatcher,
        { it as? ExecutorCoroutineDispatcher })

Element#get() 方法中的实现就是通过判断 Key 是否是当前对象的 Key,如果是就返回当前 Element 对象,如果不是就返回空。

fold() 方法在 Kotlin 中的很多集合类有这个方法,它是用来遍历所有的元素然后把他们组合成一个新的对象返回,Element#fold() 也是一样的功能,不过它只有一个元素。

Element#minusKey() 方法是用来移除某个 Key 的元素,Element 中就只有一个元素,如果传入的 Key 是当前 ElementKey 就返回 EmptyCoroutineContext,反之就返回自己。

CombinedContext

Kotlin 复制代码
@SinceKotlin("1.3")
internal class CombinedContext(
    private val left: CoroutineContext,
    private val element: Element
) : CoroutineContext, Serializable {

    override fun <E : Element> get(key: Key<E>): E? {
        var cur = this
        // 遍历查找
        while (true) {
            // 如果当前的节点的 get() 返回不为空就表示这就是目标的 Element,直接返回
            cur.element[key]?.let { return it }
            // 下一个节点
            val next = cur.left
            // 如果下一个节点是 CombinedContext,继续遍历查找。
            if (next is CombinedContext) {
                cur = next
            } else {
                // 下一个节点不是 CombinedContext,直接调用 get() 方法返回。
                return next[key]
            }
        }
    }

    public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
        operation(left.fold(initial, operation), element)

    public override fun minusKey(key: Key<*>): CoroutineContext {
        // 如果当前 Element 的 key 就是需要移除的 Element,直接返回下一个节点
        element[key]?.let { return left }
        // 调用上一个节点的 minusKey 方法,返回值为移除后对象
        val newLeft = left.minusKey(key)
        return when {
            // 新的和旧的没有改变表示没有找到对应的 key,直接返回当前对象。
            newLeft === left -> this
            // 新的下一个节点为空,直接返回 element
            newLeft === EmptyCoroutineContext -> element
            // 重新组合成一个新的 CombinedContext 对象。
            else -> CombinedContext(newLeft, element)
        }
    }

    // ...
}

CombinedContext 的实现可以理解为是一个 Element 的链表结构,其中 element 表示当前元素,left 表示指向下一个节点。

CombinedContext#get() 方法中依次查询每个节点的 Key 是否和 Element 对应,如果对应就直接返回。

CombinedContext#fold() 没什么好说的,依次调用每个 Elementfold() 方法最后得到组合后的对象。

CombinedContext#minusKey() 方法是用来移除某个 Key 的元素,返回值是被移除 Key 后新的 CoroutineContext,它这里的实现不是通过直接删除当前对象的某个元素,而是返回一个新的删除某个元素后的新对象。这种思想非常重要,用新的组合来替代修改,在 Kotlin 协程的实现中有很多处都体现了这种思想,这种实现能够更容易地避免 Bug,而且可以逃避一些锁的操作。

组合两个 CoroutineContext

在上面一节了解到用来描述组合后的 CoroutineContext,用的是 CombinedContext 这个对象,那么如何组合呢?

Kotlin 复制代码
val context1 = Dispatchers.Default  
val context2 = Job()  
val combinedContext = context1 + context2

像以上代码一样直接用 + 符号就好了,如果你不知道 Kotlin 的符号重载和中坠函数,我建议你去找找相关的资料学习下。这个函数的实现是 CoroutineContext#plus() 方法:

Kotlin 复制代码
    public operator fun plus(context: CoroutineContext): CoroutineContext =
        if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
            // 遍历合并被加的 Context,合并时初始化的值为当前对象。
            context.fold(this) { acc, element ->
                // 移除参数中的 Element (也就是原来的 Element 与参数中的 Element有冲突,参数中的 Element 优先级高)
                val removed = acc.minusKey(element.key)
                if (removed === EmptyCoroutineContext) element else {
                    // make sure interceptor is always last in the context (and thus is fast to get when present)
                    // 以下的操作是始终让 ContinuationInterceptor 在链表的头部
                    val interceptor = removed[ContinuationInterceptor]
                    if (interceptor == null) CombinedContext(removed, element) else {
                        val left = removed.minusKey(ContinuationInterceptor)
                        if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
                            CombinedContext(CombinedContext(left, element), interceptor)
                    }
                }
            }

Element 出现冲突时,参数中的 Element 的优先级比当前对象的 Element 要高;始终会将 ContinuationInterceptor 放在链表的第一个元素,因为协程运行时获取这个元素的频率很高。

最后

CoroutineContext 中的链表实现方式很有意思,而且在 Kotlin 的实现中很多的地方都用了组合的思想来替代修改。很多时候 var 就是造成 BUG 的元凶,同时也会造成多线程编程困难。而 val 就能够在一定的程度上缓解这种问题。

相关推荐
coder_pig2 小时前
🤡 公司Android老项目升级踩坑小记
android·flutter·gradle
死就死在补习班4 小时前
Android系统源码分析Input - InputReader读取事件
android
死就死在补习班4 小时前
Android系统源码分析Input - InputChannel通信
android
死就死在补习班4 小时前
Android系统源码分析Input - 设备添加流程
android
死就死在补习班4 小时前
Android系统源码分析Input - 启动流程
android
tom4i4 小时前
Launcher3 to Launchpad 01 布局修改
android
雨白4 小时前
OkHttpClient 核心配置详解
android·okhttp
淡淡的香烟4 小时前
Android auncher3实现简单的负一屏功能
android
RabbitYao5 小时前
Android 项目 通过 AndroidStringsTool 更新多语言词条
android·python
RabbitYao5 小时前
使用 Gemini 及 Python 更新 Android 多语言 Excel 文件
android·python