协程 CoroutineContext

158 阅读3分钟

概念

CoroutineContext 说白了记录的是:协程运行所需要的属性,如调度器、Job 等。这些属性在协程中被叫做 Element,每一个都有唯一的 key 进行标记。

CoroutineContext 以链表结构管理所有的 element,链表的节点被记为 CombinedContext:内部通过 left 指向链表的前一个节点 —— 有可能是单纯的 element,也有可能是另一个 CombinedContext。

同时这些 Element 可以支持加、减操作,现在看一下协程内部是如何对 Element 进行管理的。

加操作

加操作是通过重写 plus() 函数进行实现的,代码如下

public operator fun plus(context: CoroutineContext): CoroutineContext =
    if (context === EmptyCoroutineContext) 
        // 如果 context 是空的,不需要执行任何操作
        this 
    else // fast path -- avoid lambda creation
        // 否则对 element 进行累加操作
        // fold 操作逻辑是:从左到右遍历 context 中所有 element
        // 例如有 element 1,2,3
        // 那么 fold 的 lambda 第一次操作是 1,2
        // 第二次参数是:上一次 lambda 的返回值,3
        // 因此 fold 会遍历到最开始的两个 element
        context.fold(this) { acc, element ->
            // 对 context 执行减法操作
            // 如果 acc 中不含 element.key,返回的结果还是 acc
            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)
                val interceptor = removed[ContinuationInterceptor]
                if (interceptor == null) 
                    // 如果没有 ContinuationInterceptor 就直接返回 CombinedContext
                    CombinedContext(removed, element) 
                 else {
                    val left = removed.minusKey(ContinuationInterceptor)
                    if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
                        CombinedContext(CombinedContext(left, element), interceptor)
                }
            }
        }
// 对于普通的 element,其 fold 逻辑如下:
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
    operation(initial, this)

举例说明整个加操作的流程,比如

e1 + e2 + e3

从左往右,先看 e1 + e2,相当于调用 e2.fold(e1),最终得到 CombinedContext(e1,e2),表达式转为 CombinedContext(e1,e2) + e3,结合 fold() 逻辑最终结果就成 CombinedContext(CombinedContext(e1,e2),e3)

在不考虑 ContinuationInterceptor 前提下,回顾整个 plus 逻辑可发现协程是以如下形式组织管理所有 element

image.png

上图中每一个方框表示一个 CombinedContext下图中每一个方框表示一个 CombinedContext,最终程序拿到的是 CombinedContext,它通过 left 依次指向前面的 context。

减操作

实际使用中并没有办法像使用 + 号一样使用 - 号,但在 plus() 方法中调用了 minusKey(),故且称之为减操作。

减操作也分两种,普通的 element 逻辑如下

// 如果 key 相同,相当于将当前 element 从 context 中删除
// 所以返回 empty
// 否则跟当前 element 无关,直接返回当前 element 即可
public override fun minusKey(key: Key<*>): CoroutineContext =
    if (this.key == key) EmptyCoroutineContext else this

另一种是 CombinedContext 的减操作,每一个 CombinedContext 通过 left 指向它左侧的 context,通过 element 指向当前操作的 element

public override fun minusKey(key: Key<*>): CoroutineContext {
    // 如果当前操作的 element 要被减去,直接返回它左侧的 ctx 即可
    element[key]?.let { return left }
    // 否则,递归使用左侧 context 进行减操作
    val newLeft = left.minusKey(key)
    return when {
        // 左侧 context 中也不含有 key 对应有 element,该次操作不会产生任何效果
        // 返回 this 即可
        newLeft === left -> this 
        // 左侧 context 就是要减去的 element,返回 element 即可
        newLeft === EmptyCoroutineContext -> element
        // 否则说明左侧 ctx 有变化,需要重新封闭一个
        else -> CombinedContext(newLeft, element)
    }
}

获取

获取逻辑就很简单:在链表上递归查找没有 key 对应的 element 即可。

override fun <E : Element> get(key: Key<E>): E? {
    var cur = this
    while (true) {
        // 如果当前就有,直接可以返回了
        cur.element[key]?.let { return it }
        // 否则通过 left 指向前一个节点,再次查找
        val next = cur.left
        if (next is CombinedContext) {
            cur = next
        } else {
            return next[key]
        }
    }
}

总结

context 的整体逻辑很简单,通过 get() 的逻辑可以发现:后加的同 key 的 element 会覆盖先加的 element