协程上下文(CoroutineContext)是 Kotlin 协程中用于承载调度器、异常处理器、命名参数等元素的不可变容器。它的内部实现采用左偏结构:合并时右侧(新加入)元素优先,相同 Key 的元素由右侧覆盖左侧。
1. CoroutineContext 的 plus 函数
kotlin
public operator fun plus(context: CoroutineContext): CoroutineContext =
// 空上下文快速路径:右侧为空时直接返回左侧,无需创建新对象
if (context === EmptyCoroutineContext) this
else context.fold(this) { acc, element ->
// fold 会把右侧 context 里的每个 element 逐个拿到这里。
// acc 是已经合并到左侧的上下文,初始值就是 this。
// 先在 acc 中删掉与当前 element 同 Key 的旧元素,实现"后写入覆盖先写入"
val removed = acc.minusKey(element.key)
if (removed === EmptyCoroutineContext)
// 删完后 acc 变空了,说明原来只有这一个 Key,直接返回新元素即可
element
else {
// 剩下的 removed 中可能还包含 ContinuationInterceptor(调度器)。
// Kotlin 协程要求一个上下文里最多只有一个调度器,且位置固定在最右侧,
// 所以要把它单独抽出来,最后再放回最右边。
val interceptor = removed[ContinuationInterceptor]
if (interceptor == null)
// 没有调度器,普通元素直接拼到 removed 右侧
CombinedContext(removed, element)
else {
// 有调度器:先把调度器从 removed 中摘掉,得到剩余部分 left
val left = removed.minusKey(ContinuationInterceptor)
if (left === EmptyCoroutineContext)
// 摘掉调度器后只剩空上下文,说明调度器就是 removed 里的全部。
// 按照规则,调度器要放在最右边,所以新 element 在左,调度器在右
CombinedContext(element, interceptor)
else
// 剩余部分不为空:先把剩余部分 left 与新 element 组合,
// 再把调度器 interceptor 固定放到最右侧
CombinedContext(CombinedContext(left, element), interceptor)
}
}
}
代码说明
-
if (context === EmptyCoroutineContext) this如果要合并的是空上下文,直接返回自身,避免无意义的装箱。
-
context.fold(this) { acc, element -> ... }对右侧
context做 fold,将其中的每一个element逐个合并到左侧累加器acc(初始为this)中。 -
val removed = acc.minusKey(element.key)在
acc中移除与当前element同 Key 的元素,实现后写入覆盖先写入。 -
if (removed === EmptyCoroutineContext) element如果移除后
acc为空,说明acc里只有这一个 Key,直接返回当前element。 -
对
ContinuationInterceptor的特殊处理调度器(ContinuationInterceptor)在 Kotlin 协程中被视为"全局单例"元素。合并时,会先把已有的 interceptor 从
removed中拿出来,再把剩余部分与新element组合,最后把 interceptor 固定放到最右侧。这保证:markdown- 一个 `CoroutineContext` 中最多只有一个 `ContinuationInterceptor`; - 查找时 interceptor 出现在稳定位置,便于底层实现统一拦截。
2. CombinedContext
kotlin
// CombinedContext 是一棵向右倾斜的二叉树:
// left 是左侧子上下文(可能是另一个 CombinedContext,也可能是 EmptyCoroutineContext 或单元素)
// element 是当前节点携带的单个 Element
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) {
// 先查当前节点的 element;命中直接返回(右优先)
cur.element[key]?.let { return it }
// 没命中就往左走
val next = cur.left
if (next is CombinedContext) {
// 左侧还是 CombinedContext,继续向下查找
cur = next
} else {
// 左侧已经是基础上下文(EmptyCoroutineContext 或单元素),交给它查
return next[key]
}
}
}
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
// 后序遍历:先把左侧子上下文 fold 完,再把当前 element 传给 operation。
// 这样 fold 的遍历顺序与 plus 合并时的顺序一致:从左到右。
operation(left.fold(initial, operation), element)
public override fun minusKey(key: Key<*>): CoroutineContext {
// 当前节点的 element 匹配 key:摘掉当前节点,返回左侧子树
element[key]?.let { return left }
// 当前节点不匹配,递归到左侧继续删
val newLeft = left.minusKey(key)
return when {
// 左侧没变,说明要删的 key 不在整个树里,直接返回原对象
newLeft === left -> this
// 左侧删空了,只剩当前 element
newLeft === EmptyCoroutineContext -> element
// 左侧删除了部分内容,用新的 left 重新组装 CombinedContext
else -> CombinedContext(newLeft, element)
}
}
}
代码说明
CombinedContext 是 plus 操作最常见的产物,它不是扁平列表,而是一棵向右倾斜的二叉树:
left:另一个CoroutineContext(可能是另一个CombinedContext,也可能是EmptyCoroutineContext或单元素);element:当前节点携带的单个Element。
2.1 get(key) 查找逻辑
- 从当前节点开始,先查
element;未命中则递归进入left。 - 若
left仍是CombinedContext则继续向下,否则进入最后的基础上下文。 - 由于右侧元素先被访问,天然实现右覆盖左。
2.2 fold(initial, operation) 折叠逻辑
- 采用后序遍历思路:先对
left做 fold,再把当前element传入operation。 - 因此 fold 的遍历顺序是从左到右 ,与
plus的合并语义一致。
2.3 minusKey(key) 删除逻辑
- 如果当前
element的 Key 匹配,直接返回left(相当于摘掉当前节点); - 否则对
left递归执行minusKey; - 若
left没有变化,返回this; - 若
left被删空,返回当前element; - 否则用新的
left与当前element重新包装成CombinedContext。
3. 左偏结构说明
协程上下文本质上是一个不可变的元素集合,每个元素通过 Key 标识。所谓左偏结构,是指当两个上下文通过 plus 操作符合并时:
- 右侧(后添加)上下文中的元素在查找时具有更高优先级;
- 若两侧存在相同 Key 的元素,右侧元素会覆盖左侧元素;
- 整个结构以左侧为基底、右侧为增量,因此呈现左偏形态。
直观理解:
CoroutineContext类似于一个只读的Map<Key, Element>;- 合并时不是简单追加,而是按 Key 去重并保证后写入者生效;
- 合并结果通常用
CombinedContext表示,它是一个以left + element为节点的二叉树状结构。
4. 总结
CoroutineContext通过plus实现不可变合并,遵循右优先、左偏规则。CombinedContext使用树状结构高效共享子上下文,删除 / 查找都按 Key 递归完成。ContinuationInterceptor在合并时被特殊处理,保证协程调度器唯一且位置最右。