前言
翻译自 协程异常
这一章节将介绍在协程中异常是如何传播的,以及如何通过不同的方法处理他们。
协程突然失败了,怎么办
如果一个协程exception了,会将上述异常传给它的父级。之后,父级会1.取消其他的子级2. 取消自己3.传递异常给他的父级
这个异常会传达到根级,所有这个scope开始的协程都会被取消。
尽管在某些情况下传播异常可能很有意义,但也有一些不期望传异常的。想象一个专门处理用户操作的与UI有关的scope。如果一个子协程抛出异常,UI scope也会被取消,整个UI 组件都会没有反应,因为一个取消掉的scope不能再开始更多的协程.
那该怎么办呢?或者,可以在创建这些协程的CoroutineScope的CoroutineContext中使用Job的不同实现,即SupervisorJob。
SupervisorJob拯救
使用SupervisorJob ,child的失败将不会影响其他的child。SupervisorJob不会取消自己或者其他的子级。而且,SupervisorJob不会传递异常,并且允许child去处理异常。
你可以用类似val uiScope = CoroutineScope(SupervisorJob())创建一个scope,这样当一个协程失败了,也不会去传递cancellation.
如果这个异常没有处理,并且CoroutineContext没有CoroutineExceptionHandler(后续会讲),它将会传到默认的线程ExceptionHandler。在虚拟机中,这个异常会被打印在console中,在安卓中,它会让你的APP crash,无论是在哪个dispatcher上发生的。
note: 没有被拦截的异常总会被抛出,不管你用的哪种job。
同样的行为适用于coroutinescope和supervisorscope。这些将创建一个子scope(相应的job或SupervisorJob作为父级),这样你可以有逻辑的组织协程。(例如。做并行的计算或者让他们互相影响或互不影响)
注意:仅当supervisorJob作为scope的一部分才可以像描述的那样:使用supervisorScope或CoroutineScope(SupervisorJob())创建
Job还是SupervisorJob?
用SupervisorJob 或者supervisorScope当你不想要失败的时候去取消父级和同级。
例如
val scope = CoroutineScope(SupervisorJob())
scope.launch {
// Child 1
}
scope.launch {
// Child 2
}
在这种情况下,如果child1失败了,不会取消child2和scope。
例如,
val scope = CoroutineScope(Job())
scope.launch {
supervisorScope {
launch {
// Child 1
}
launch {
// Child 2
}
}
}
这个例子,supervisorScope用supervisorJob创建了一个子scope,如果child1失败了,child2不会被取消。
相反,如果使用coroutineScope,失败将会传递并且也会取消掉scope。
谁是我的父级?
你能辨别出下面的代码中谁是child1的父级吗?
val scope = CoroutineScope(Job())
scope.launch(SupervisorJob()) {
// new coroutine -> can suspend
launch {
// Child 1
}
launch {
// Child 2
}
}
child1的父级是job类型,即使第一印象你觉得是supervisorjob类型,但那是不对的。不是因为一个新的协程总会创建一个新的job,而是因为在这种情况下,会覆盖掉supervisorjob。supervisorjob是scope.launch创建的协程的父级,SupervisorJob在这段代码里啥也没干,也就是没啥用。
因此,无论child1.还是child2失败,这个失败都会到达scope,所有被这个scope开始的工作都会被cancel掉。
记住supervisorjob只有在使用supervisorScope或者CoroutineScope(SupervisorJob())才有用 。作为创建协程的参数的supervisorJob并不会有你期待的cancellation作用。
对于异常,如果一个子级抛出异常,supervisorjob不会传递异常,并允许自己的协程去处理它。
幕后工作
如果你好奇job的幕后工作感到好奇,可以查看JobSupport.kt文件的childCancelled和notifycabcelling方法。
在SupervisorJob的实现类中,childCancelled返回了false,这意味着它不会传递取消但也不处理异常。
异常处理
协程使用常规的Kotlin语法来处理异常:try / catch或内置帮助程序功能(如runCatching(内部使用try / catch))
我们之前说过总会抛出不被捕获的异常。然而,不同协程构建器用不同的方式处理异常。
Launch
异常会在发生的时候立刻抛出。因此,可以这样子
scope.launch {
try {
codeThatCanThrowExceptions()
} catch(e: Exception) {
// Handle exception
}
}
Async
当async用作根协程时(是CoroutineScope实例或supervisorScope的直接子级),异常不会立刻抛出,而是在调用.await()时抛出。例如
supervisorScope {
val deferred = async {
codeThatCanThrowExceptions()
}
try {
deferred.await()
} catch(e: Exception) {
// Handle exception thrown in async
}
}
在这个示例中,注意调用async永远不会抛出异常,await才会抛出async协程的异常。
注意,我们使用的是supervisorScope去调用async和await。就像我们之前说的,supervisorjob允许协程去处理异常。相反的,如果用job将会自动将异常向上传递,因此catch块无用。
coroutineScope {
try {
val deferred = async {
codeThatCanThrowExceptions()
}
deferred.await()
} catch(e: Exception) {
// 异常永远不会被这里捕获,而是会向上传给scope
}
}
此外,通过其他协程创建的协程发生异常,一定会被传递,无论使用的哪种构造器。例如
val scope = CoroutineScope(Job())
scope.launch {
async {
// 如果async抛出异常,launch会抛出而不需要调用.await()
}
}
在这个例子中,如果async会在异常发生的时候立刻抛出异常,因为协程是scope launch的直接子级。因为async(job在它自己的CoroutineContext中)会自动的将异常传给它的父级(launch)来抛出异常。
注意:在CoroutineScope构造器中或通过其他协程创建的协程抛出的异常都不能被try/catch捕获
在SupervisorJob小节中,我们提到了CoroutineExceptionHandler的存在。接下来让我们深入研究它。
CoroutineExceptionHandler
CoroutineExceptionHandler是CoroutineContext的一种可选择元素,允许你处理未捕获的异常。
下面是定义CoroutineExceptionHandler的示例,无论何时捕获到一个异常,你都会有关于CoroutineContext和异常本身的相关信息。
val handler = CoroutineExceptionHandler {
context, exception -> println("Caught $exception")
}
如果满足一下要求,exception会被捕获:
when: 协程会自动抛出异常(launch,而非async)
where: 在CououtineScope的CoroutineContext中或根协程(CoroutineScope或supervisorScope的直接子级)
在下面这个例子中,exception会被handler捕获
val scope = CoroutineScope(Job())
scope.launch(handler) {
launch {
throw Exception("Failed coroutine")
}
}
而在下面这个例子中,exception不会被捕获
val scope = CoroutineScope(Job())
scope.launch {
launch(handler) {
throw Exception("Failed coroutine")
}
}
因为这个handler没有被放置在正确的CoroutineContext中。内置的launch会立刻传递异常给父级,父级不会知道关于这个handler一无所知,异常会被抛出。
优雅的处理异常是很重要的,记住用SupervisorJob当你想在异常发生时避免传递cancellation,否则用job。
后记
下一节学习不应该被取消的工作