opoojkk

Kotlin 协程:Job 与 SupervisorJob 的差异

lxx
目次

我们都知道,在 Kotlin 协程中,可以使用 JobSupervisorJob 来构建一个作用域(CoroutineScope):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// SupervisorJob        
val scope = CoroutineScope(SupervisorJob())
scope.launch {
    ...
}

// Job
val scope = CoroutineScope(Job())
scope.launch {
    ...
}

两者的区别似乎仅在于构造函数不同。点开源码一看,差别的确只有一个方法:

1
2
3
4
5
6
7
public fun Job(parent: Job? = null): CompletableJob = JobImpl(parent)

public fun SupervisorJob(parent: Job? = null): CompletableJob = SupervisorJobImpl(parent)

private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
    override fun childCancelled(cause: Throwable): Boolean = false
}

SupervisorJobImpl 继承自 JobImpl,唯一做的事就是重写了 childCancelled 方法。 也就是说,整个差异的核心就在这里。

JobImpl 的取消逻辑 #

来看 JobImpl 的实现:

1
2
3
4
public open fun childCancelled(cause: Throwable): Boolean {
    if (cause is CancellationException) return true
    return cancelImpl(cause) && handlesException
}

当子协程异常时,JobImpl 默认会执行 cancelImpl(cause), 也就是会触发父协程的取消逻辑。

cancelImpl 中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
internal fun cancelImpl(cause: Any?): Boolean {
    var finalState: Any? = COMPLETING_ALREADY
    if (onCancelComplete) {
        finalState = cancelMakeCompleting(cause)
        if (finalState === COMPLETING_WAITING_CHILDREN) return true
    }
    if (finalState === COMPLETING_ALREADY) {
        // 执行取消
        finalState = makeCancelling(cause)
    }
    return when {
        finalState === COMPLETING_ALREADY -> true
        finalState === COMPLETING_WAITING_CHILDREN -> true
        finalState === TOO_LATE_TO_CANCEL -> false
        else -> {
            afterCompletion(finalState)
            true
        }
    }
}

也就是说,一旦子协程抛出异常,JobImpl 就会触发父协程的 makeCancelling() 流程, 继而执行 notifyCancelling(),从而传播取消信号

1
2
3
4
5
6
7
private fun notifyCancelling(list: NodeList, cause: Throwable) {
    // 取消子任务
    onCancelling(cause)
    notifyHandlers<JobCancellingNode>(list, cause)
    // 取消父任务
    cancelParent(cause)
}

SupervisorJob 的不同 #

SupervisorJobImplchildCancelled 方法返回 false。 这意味着:当子协程异常取消时,父协程不会被取消

这一点正是 SupervisorJob 的核心特性:

子协程的失败不会影响到其他兄弟协程,也不会导致整个作用域取消。

childCancelled 的调用链 #

继续往下看,在 ChildHandleNode 中:

1
2
3
4
5
6
7
8
private class ChildHandleNode(
    @JvmField val childJob: ChildJob
) : JobNode(), ChildHandle {
    override val parent: Job get() = job
    override val onCancelling: Boolean get() = true
    override fun invoke(cause: Throwable?) = childJob.parentCancelled(job)
    override fun childCancelled(cause: Throwable): Boolean = job.childCancelled(cause)
}

这里当子任务被取消时,会通过 childCancelled 将异常传递回父任务。 而 SupervisorJobImpl 正是通过重写 childCancelled 来“截断”这个传播链。

cancelParent 的触发点 #

那么,cancelParent 是在哪里被调用的? 在 JobSupport.kt 中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
private fun cancelParent(cause: Throwable): Boolean {
    if (isScopedCoroutine) return true

    val isCancellation = cause is CancellationException
    val parent = parentHandle

    if (parent === null || parent === NonDisposableHandle) {
        return isCancellation
    }

    // 通知父任务
    return parent.childCancelled(cause) || isCancellation
}

从注释可以看出:

对于正常的 CancellationException,不会取消父任务; 只有当出现其他异常(例如崩溃)时,才可能向父任务传播。

这个方法会在 notifyCancelling()finalizeFinishingState() 中被调用。 而后者正是任务完成状态的最终确定逻辑。

finalizeFinishingState:异常如何进入“最终状态” #

finalizeFinishingState 中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private fun finalizeFinishingState(state: Finishing, proposedUpdate: Any?): Any? {
    val proposedException = (proposedUpdate as? CompletedExceptionally)?.cause

    val finalException = synchronized(state) {
        val exceptions = state.sealLocked(proposedException)
        val finalCause = getFinalRootCause(state, exceptions)
        if (finalCause != null) addSuppressedExceptions(finalCause, exceptions)
        finalCause
    }

    val finalState = when {
        finalException == null -> proposedUpdate
        finalException === proposedException -> proposedUpdate
        else -> CompletedExceptionally(finalException)
    }

    if (finalException != null) {
        val handled = cancelParent(finalException) || handleJobException(finalException)
        if (handled) (finalState as CompletedExceptionally).makeHandled()
    }

    completeStateFinalization(state, finalState)
    return finalState
}

这里的关键在于:

handled 状态的意义 #

makeHandled() 的作用是将 CompletedExceptionally 标记为已处理(handled = true)。 这个标志会影响 onCompletionInternal 的行为:

1
2
3
4
5
6
protected final override fun onCompletionInternal(state: Any?) {
    if (state is CompletedExceptionally)
        onCancelled(state.cause, state.handled)
    else
        onCompleted(state as T)
}

也就是说,当 handledfalse 时,异常会被进一步上报。

SupervisorJob 中,由于 childCancelled 返回 false, 子任务的异常不会让父协程进入取消状态,也不会设置 handled = true。 结果就是:异常只会在当前子协程内抛出,不会影响其他子协程。

结论 #

SupervisorJob 通过重写 childCancelled,阻止了子协程异常向父协程传播,从而让协程之间相互独立。

在 Job 中,任何子协程的异常都会触发 cancelImpl → notifyCancelling → cancelParent 链条,导致整个作用域被取消。 而在 SupervisorJob 中,这条链被截断,父协程不再传播取消信号,异常只在本地处理。

对比项JobSupervisorJob
子任务异常会取消父任务不会取消父任务
兄弟任务影响相互影响(一个失败全部取消)相互独立(失败互不影响)
childCancelled触发父级取消逻辑返回 false,忽略子取消
handled 状态置为 true(异常被处理)仍为 false(异常局部处理)

所以,当你希望在一个作用域中,某个子任务失败时不影响其他任务SupervisorJob 就是你要用的那个。

标签:
Categories:

评论

评论由 giscus 提供;如果未加载,可能是网络环境阻止了 giscus。