协程顺序启动与嵌套启动为何存在差异?SupervisorJob行为解析
场景1:符合预期(能看到"after delay"日志)
以下代码中,SupervisorJob的结构化并发表现符合预期,最终会输出"after delay"日志:
val exceptionHandler = CoroutineExceptionHandler { _, ex -> Log.e("coroutine", "exceptionHandler exeption is $ex") } val customScope = CoroutineScope(Job()) val superScope = CoroutineScope(SupervisorJob() + exceptionHandler) customScope.launch { superScope.launch { launch { Log.e("coroutine", "before delay context is $coroutineContext") delay(5000) Log.e("coroutine", "after delay") } } superScope.launch { launch { Log.e("coroutine", "RuntimeException coming context is $coroutineContext") delay(3000) throw RuntimeException() } } }
这里两个superScope.launch是SupervisorJob的直接子协程,属于同级独立分支。SupervisorJob的核心特性就是单个子分支抛出异常时,不会影响其他同级子分支的执行,所以抛出异常的协程被取消后,另一个分支的协程仍能正常执行到"after delay"。
场景2:不符合预期(看不到"after delay"日志)
下面的代码无法输出"after delay"日志,表现不符合预期:
val exceptionHandler = CoroutineExceptionHandler { _, ex -> Log.e("coroutine", "exceptionHandler exeption is $ex") } val customScope = CoroutineScope(Job()) val superScope = CoroutineScope(SupervisorJob() + exceptionHandler) customScope.launch { superScope.launch { launch { Log.e("coroutine", "before delay context is $coroutineContext") delay(5000) Log.e("coroutine", "after delay") } launch { Log.e("coroutine", "RuntimeException coming context is $coroutineContext") delay(3000) throw RuntimeException() } } }
关键差异在于:这里两个内部launch的父协程是superScope.launch创建的普通Job(而非SupervisorJob)。普通Job的行为是当任意子协程抛出异常时,会取消所有子协程并向上传播异常,因此等待delay的协程会被提前取消,无法输出"after delay"。
场景3:指定上下文后恢复正常
如果给抛出异常的协程显式指定superScope的上下文,就能恢复预期行为:
val exceptionHandler = CoroutineExceptionHandler { _, ex -> Log.e("coroutine", "exceptionHandler exeption is $ex") } val customScope = CoroutineScope(Job()) val superScope = CoroutineScope(SupervisorJob() + exceptionHandler) customScope.launch { superScope.launch { launch { Log.e("coroutine", "before delay context is $coroutineContext") delay(5000) Log.e("coroutine", "after delay") } launch (superScope.coroutineContext){ Log.e("coroutine", "RuntimeException coming context is $coroutineContext") delay(3000) throw RuntimeException() } } }
这里显式传入superScope.coroutineContext,让抛出异常的协程直接挂载到SupervisorJob下,成为它的直接子节点,不再受中间普通Job的约束。此时异常只会终止自身分支,不会影响同级的另一个协程。
核心疑问
为何同级启动的协程与嵌套启动的协程表现不同?原本认为协程上下文是父上下文+自身Job,同级启动的上下文应与嵌套的一致。
更新后的理解
经过验证,运行机制可总结为:以SupervisorJob为根的协程树中,若某分支发生错误,只会终止该分支内的协程(从出错节点向上到根节点的该分支路径),不会影响其他独立分支。
验证代码如下:
val exceptionHandler = CoroutineExceptionHandler { _, ex -> Log.e("coroutine", "exceptionHandler exeption is $ex") } val customScope = CoroutineScope(Job()) val superScope = CoroutineScope(SupervisorJob() + exceptionHandler) customScope.launch { superScope.launch { launch { Log.e("coroutine", "error branch try take result") delay(5000) Log.e("coroutine", "RESULT") } launch { launch { delay(3000) Log.e("coroutine", "error branch take some result of other branch") } } launch { launch { launch { Log.e("coroutine", "error branch RuntimeException coming") delay(4000) throw RuntimeException() } } } delay(2000) Log.e("coroutine", "error branch take some result from start branch") } superScope.launch { launch { Log.e("coroutine", "NOT error branch try take result") delay(5000) Log.e("coroutine", "RESULT from not error branch") } } }
在这个例子中,抛出异常的分支会被终止,但superScope下的另一个独立协程("NOT error branch")不受影响,仍能输出结果;而错误分支内的部分协程会因父Job被取消而无法完成(比如等待5秒的"RESULT"日志不会输出)。
内容的提问来源于stack exchange,提问作者Makc

