Kotlin 协程上下文(CoroutineContext)深度解析

64 阅读3分钟

一、CoroutineContext:行为像 Map 的链表结构

类似 Map 的特性

CoroutineContext 表现为不可变映射集合,可通过 Key 获取对应 Element(如 coroutineContext[Job])。同一上下文中每个 Key 只能对应一个 Element。

底层实现为链表

与 Java/Kotlin 的 HashMap 不同,CoroutineContext 的核心实现类 CombinedContext单向链表

  • 使用 + 运算符合并上下文时(如 context1 + context2),系统会创建新的 CombinedContext 节点
  • 新节点的 left 指向原上下文,element 指向新加入的元素
  • 该设计使上下文组合高效(无需复制整个集合),但查找元素时需遍历链表

二、Key 的单例引用机制

Key 的定义

每个 CoroutineContext.Element 都有 key 属性,类型为 CoroutineContext.Key<E>

引用比较(Reference Equality)

获取元素时,Kotlin 协程库使用引用相等性===)而非值相等性(==)来比较 Key。这意味着从上下文中取出元素时,需传入与该元素内部存储完全相同的 Key 对象实例。

因此标准 Key(如 Job.KeyCoroutineName.KeyContinuationInterceptor.Key)通常定义为单例对象object)或伴生对象中的静态单例。

注意:若使用实例不同但内容相同的 Key 对象查找,将无法找到对应元素。


三、上下文组合与覆盖规则

+ 运算符

contextA + contextB 时,若 contextB 包含相同 Key 的元素,则会覆盖 contextA 中的对应元素。

查找顺序

查找从链表右侧(最新元素)向左遍历,找到匹配 Key 即返回,保证后加入覆盖先加入的语义。


四、父子协程上下文的独立性

每当启动新协程时,系统都会创建独立的 CoroutineContext 实例。由于底层基于 CombinedContext 单向链表实现,父协程与子协程在内存中维护着两条独立链表


五、Job 父子关系的查询

Job 通过 parentchildren 属性维护树状结构,支撑结构化并发。利用这两个引用可管理协程生命周期、取消传播与异常处理。

5.1 ‌协程 Job 的父子关联示例

val parentJob = Job()
val scope = CoroutineScope(parentJob)
scope.launch {
    val currentJob  = this.coroutineContext[Job]
    println("当前 Job: $currentJob")
    println("父 Job: $parentJob")
}

5.2 查找子 Job

val parentJob = Job()
val scope = CoroutineScope(parentJob)
​
// 启动两个子协程
val child1 = scope.launch { delay(1000) }
val child2 = scope.launch { delay(2000) }
​
// 查找子 Jobs
val children = parentJob.children.toList()
​
println("子协程数量: ${children.size}") // 输出: 2
println("包含 child1? ${children.contains(child1)}") // 输出: true
println("包含 child2? ${children.contains(child2)}") // 输出: true// 遍历子协程
parentJob.children.forEach { childJob ->
    println("子 Job: $childJob,  isActive: ${childJob.isActive}")
}

六、SupervisorJob 的异常隔离机制

6.1 子 Job 的通知机制

当子协程因异常或取消而结束时,会调用 JobSupport 中的 cancelParent 方法。该方法通过 parentHandle(即父 Job)向上发送通知。

private fun cancelParent(cause: Throwable): Boolean {
    // 1. 判断是否是正常的取消异常
    val isCancellation = cause is CancellationException
    
    // 2. 获取父协程的句柄 (即 SupervisorJob 或普通 Job)
    val parent = parentHandle
    
    // 如果没有父节点,直接返回
    if (parent === null || parent === NonDisposableHandle) {
        return isCancellation
    }
    
    // 3. 【关键步骤】调用父节点的 childCancelled 方法
    // 如果父节点返回 true,或者原因是 CancellationException,则当前协程认为父节点已处理(或需要传播取消)
    return parent.childCancelled(cause) || isCancellation
}

6.2 SupervisorJob 的响应策略

internal class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
    override fun childCancelled(cause: Throwable): Boolean {
        // 1. 如果是 CancellationException,交给父类处理(通常返回 true,允许正常取消传播)
        if (cause is CancellationException) {
            return super.childCancelled(cause)
        }
        
        // 2. 【关键区别】对于其他异常(如 RuntimeException),直接返回 false
        // 这意味着:"我收到了子协程的失败通知,但我决定不取消我自己,也不向我的父节点传播这个错误"
        return false
    }
}

核心差异:普通 Job 遇到子协程异常会向上传播取消,而 SupervisorJob 对非 CancellationException 异常返回 false,实现异常隔离

6.3 取消传播的底层实现

private fun notifyCancelling(list: NodeList, cause: Throwable) {
    // 1. 通知所有监听器
    notifyHandlers<JobCancellingNode<*>>(list, cause)
    
    // 2. 通知父节点(如果是根节点则忽略)
    cancelParent(cause)
}