Kotlin 协程:Job 与 SupervisorJob 的差异
lxx
目次
我们都知道,在 Kotlin 协程中,可以使用 Job 或 SupervisorJob 来构建一个作用域(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 的不同 #
而 SupervisorJobImpl 的 childCancelled 方法返回 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
}
|
这里的关键在于:
- 如果
cancelParent 返回 true,说明异常已被父协程处理; - 否则,
handleJobException 再尝试处理; - 若两者都未处理,则异常会继续冒泡到
CoroutineExceptionHandler。
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)
}
|
也就是说,当 handled 为 false 时,异常会被进一步上报。
在 SupervisorJob 中,由于 childCancelled 返回 false,
子任务的异常不会让父协程进入取消状态,也不会设置 handled = true。
结果就是:异常只会在当前子协程内抛出,不会影响其他子协程。
结论 #
SupervisorJob 通过重写 childCancelled,阻止了子协程异常向父协程传播,从而让协程之间相互独立。
在 Job 中,任何子协程的异常都会触发 cancelImpl → notifyCancelling → cancelParent 链条,导致整个作用域被取消。
而在 SupervisorJob 中,这条链被截断,父协程不再传播取消信号,异常只在本地处理。
| 对比项 | Job | SupervisorJob |
|---|
| 子任务异常 | 会取消父任务 | 不会取消父任务 |
| 兄弟任务影响 | 相互影响(一个失败全部取消) | 相互独立(失败互不影响) |
| childCancelled | 触发父级取消逻辑 | 返回 false,忽略子取消 |
| handled 状态 | 置为 true(异常被处理) | 仍为 false(异常局部处理) |
所以,当你希望在一个作用域中,某个子任务失败时不影响其他任务,
SupervisorJob 就是你要用的那个。
评论
评论由 giscus 提供;如果未加载,可能是网络环境阻止了 giscus。