CoroutineScope配SupervisorJob与supervisorScope的行为差异疑问
我在学习Kotlin协程时,了解到SupervisorJob()可以避免子协程取消牵连其他子协程——coroutineScope里某个子协程失败会取消所有子协程,但supervisorScope只会取消失败的那个。不过我尝试给CoroutineScope添加SupervisorJob作为上下文时,没得到预期结果:本来想让getData1()和getData3()的结果正常打印,结果getData2()抛出异常后所有协程都被取消了。
相关代码如下:
fun main() = runBlocking { val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> println(throwable) } val customScope = CoroutineScope(SupervisorJob() + exceptionHandler) customScope.launch { launch { getData1().also { println(it) } } launch { getData2().also { println(it) } } launch { getData3().also { println(it) } } }.join() } private suspend fun getData1(): String? { delay(1000) return "data 1" } private suspend fun getData2(): String? { delay(300) throw RuntimeException("While getting Data 2 exception happened") } private suspend fun getData3(): String? { delay(800) return "data 3" }
问题原因
你创建的customScope虽然用了SupervisorJob(),但在customScope.launch内部启动的三个子协程,它们的父Job是这个外层launch生成的普通Job,而非customScope的SupervisorJob()。SupervisorJob的隔离作用仅对其直接子协程生效,内层launch会继承外层launch的Job上下文,所以当getData2()抛出异常时,外层launch的Job会触发所有子协程取消,导致getData1()和getData3()也被终止。
解决方案
有两种方式可以实现预期的隔离效果:
方式一:用supervisorScope包裹内层协程
将三个内层launch放到supervisorScope中,让它们直接处于supervisorScope的作用域下,单个子协程失败不会影响其他协程:
fun main() = runBlocking { val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> println(throwable) } val customScope = CoroutineScope(SupervisorJob() + exceptionHandler) customScope.launch { supervisorScope { launch { getData1().also { println(it) } } launch { getData2().also { println(it) } } launch { getData3().also { println(it) } } } }.join() } private suspend fun getData1(): String? { delay(1000) return "data 1" } private suspend fun getData2(): String? { delay(300) throw RuntimeException("While getting Data 2 exception happened") } private suspend fun getData3(): String? { delay(800) return "data 3" }
方式二:让内层协程直接继承SupervisorJob上下文
修改内层launch的上下文,让它们直接继承customScope的SupervisorJob,而非外层launch的普通Job:
fun main() = runBlocking { val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> println(throwable) } val customScope = CoroutineScope(SupervisorJob() + exceptionHandler) customScope.launch { // 直接继承customScope的上下文,包含SupervisorJob launch(coroutineContext) { getData1().also { println(it) } } launch(coroutineContext) { getData2().also { println(it) } } launch(coroutineContext) { getData3().also { println(it) } } }.join() } private suspend fun getData1(): String? { delay(1000) return "data 1" } private suspend fun getData2(): String? { delay(300) throw RuntimeException("While getting Data 2 exception happened") } private suspend fun getData3(): String? { delay(800) return "data 3" }
验证结果
修改后运行代码,会输出如下内容,符合预期:
java.lang.RuntimeException: While getting Data 2 exception happened data 3 data 1
内容的提问来源于stack exchange,提问作者nasibeyyubov

