Supervising Coroutines
前言
到目前为止,我们已经讨论了异常如何在协程层次结构中传播。如果一个子线程抛出了异常,异常将传播至根协程。但是,当不想要这种行为时,会发生什么呢?例如,可能有一个具有自己作用域的 UI 组件。如果你在该作用域内创建一个子协程,并且该协程失败,而 UI 组件不得被取消。但是,如果该 UI 组件的作用域被取消了,那么所有的子任务也应该被取消。事实上,在协程工具包中有包含这种情况的工具——supervisorJob
和 supervisorScope
。
SupervisorJob
SupervisorJob
与常规的 Job
类似,唯一的区别是,取消只是向下传播。这意味着子线程抛出异常不会取消它们的父协程。让我们在示例中看看。打开 SupervisorJob.kt 文件,其中有一个空的 main
函数,用以下代码替换它:
fun main() = runBlocking {
// 1
val supervisor = SupervisorJob()
with(CoroutineScope(coroutineContext + supervisor)) {
// 2
val firstChild = launch {
println("First child throwing an exception")
throw ArithmeticException()
}
// 3
val secondChild = launch {
println("First child is cancelled: ${firstChild.isCancelled}")
try {
delay(5000)
} catch (e: CancellationException) {
println("Second child cancelled because supervisor got cancelled.")
}
}
// 4
firstChild.join()
println("Second child is active: ${secondChild.isActive}")
supervisor.cancel()
secondChild.join()
}
}
输出:
First child throwing an exception
First child is cancelled: true
Second child is active: true
Second child cancelled because supervisor got cancelled.
Exception in thread "main" java.lang.ArithmeticException ...
下面是对上述代码的解析:
- 你创建了一个
SupervisorJob
的实例,然后创建了一个新的CoroutineScope
,并将刚创建的Job
作为其上下文的一部分。 - 你创建的第一个子协程抛出
ArithmeticException()
。 - 你创建的第二个子协程先打印第一个子协程的 取消 状态,然后延迟 5 秒钟并捕获
CancellationException
,当supervisor
取消时 。 - 等待第一个子线程执行完毕。然后你打印第二个子协程的状态来确保它仍在运行。然后你取消了
supervisor
并等待第二个子线程执行完毕。此时,secondChild
中的catch
块将因CancellationException
而触发。
SupervisorScope
还记得本章之前的的 提示 吗(译者注:详见 CoroutineExceptionHandler),说 async
块有时会向上传播异常,而你无法捕获它?现在让我们检查一下那种情况,看看我们如何使用 SupervisorScope
来改变这种行为。在你的入门项目中打开 SupervisorScope.kt 并添加以下代码:
fun main() = runBlocking {
val result = async {
println("Throwing exception in async")
throw IllegalStateException()
}
try {
result.await()
} catch (e: Exception) {
println("Caught $e")
}
}
输出:
Throwing exception in async
Caught java.lang.IllegalStateException
Exception in thread "main" java.lang.IllegalStateException
如你所见,即使你在 catch
块中捕获了异常,它仍然向上传播并重新抛出。为了解决这个问题,你应该使用 supervisorScope
。它只是一个挂起函数,可以创建一个新的 CoroutineScope
,并在该作用域中运行提供的挂起块。新的作用域是使用 SupervisorJob
创建的,这意味着子协程在失败时不会相互影响。以下是应用了修复程序的相同代码:
fun main() = runBlocking {
supervisorScope {
val result = async {
println("Throwing exception in async")
throw IllegalStateException()
}
try {
result.await()
} catch (e: Exception) {
println("Caught $e")
}
}
}
输出:
Throwing exception in async
Caught java.lang.IllegalStateException
如你所见,现在异常不会向上传播并重新抛出了。这是因为 supervisorScope
让协程自己处理异常而不是传播至父协程。