我将翻译三篇介绍协程的 取消 和 异常处理 相关的文章,三篇文章是层层递进的关系。翻译过程中我将尽量忠实于原文。当然,由于水平有限,不能保证完全的翻译正确。如果您发现翻译有错误之处,欢迎在评论里指出。我也将贴出每篇翻译的原文。
- 第一篇:《协程:第一件事》(原文: Coroutines: first things first)
- 第二篇:《协程的取消》(原文:Cancellation in coroutines)
- 第三篇:《协程的异常》(原文:Exceptions in coroutines)
这是第三篇。
文章目录
我们开发者通常花费大量时间打磨应用的正常使用场景。然而,当应用发生意外时,合适的用户体验也同样重要。一方面,对用户来说应用发生崩溃是糟糕的体验;另一方面,当操作执行失败时,给用户提示适当的信息是必不可少的。
合理的异常处理将对用户如何看待你的应用产生很大的影响。在这篇文章中,我们将解释协程中的异常如何传播,以及如何通过多种方式控制它们。
协程突然失败了!怎么办?😱
当一个协程因为异常失败时,它会把异常传播给它的父节点!然后,这个父节点会 1) 取消其余的子协程,2) 取消自己, 3) 把异常传播给自己的父节点。
异常将被传播到层次结构的根节点,所有由这个根节点 CoroutineScope发起的协程都将被取消。

(协程中发生的异常将通过协程的层次结构向上传播)
有些情况下这种异常传播是合理的,然而也有些情况不需要这样。试想,有一个与UI相关的 CoroutineScope,用来处理用户交互。如果其中一个子协程抛了异常,那么这个UI scope将被取消,并且整个UI模块将变得无法响应,因为一个被取消的 scope不能再启动协程。
如果你不想要这样的行为该怎么办?作为替代,你可以在创建协程的 CoroutineScope的 CoroutineContext中用一个不同的 Job实现,名叫 SuervisorJob。
拯救者 SupervisorJob
使用 SupervisorJob,子协程的失败不会影响其余子协程。一个 SupervisorJob不会取消它自己或它的子协程。而且,SupervisorJob也不会传播异常,它会让子协程自己处理异常。
你可以这样创建一个 CoroutineScope:
val uiScope = CoroutineScope(SupervisorJob())
当一个协程失败时,它不会传播取消,如下图所示:

(一个 SupervisorJob不会因为异常取消自身或其余的子协程)
如果异常没有被处理,并且 CoroutineContext没有 CoroutineExceptionHandler,异常将到达默认线程的 ExceptionHandler。在JVM中,异常将被记录到控制台;在Android中,不管异常发生在哪个线程,都将导致app崩溃。
💥未捕获的异常总会被抛出,不管你使用哪种
Job
同样的行为也适用于 scope构建器 coroutineScope和 supervisorScope。这两种构建器将创建一个子 scope(使用 Job或 SupervisorJob),你可以用它们来组织协程(比如说你想做并行计算,或者你希望它们相互影响or相互不影响)。
提醒:SupervisorJob只有在其作为 scope的一部分时才起作用,要么使用 supervisorScope创建,要么使用 CoroutineScope(SuperVisorJob())创建。
Job 还是 SupervisorJob ?🤔
什么时候该使用 Job或者 SupervisorJob?当你不希望失败导致父节点或同级节点被取消时,就使用 SupervisorJob或者 supervisorScope。
一个例子:
val scope = CoroutineScope(SupervisorJob())
scope.launch {
// 子协程1
}
scope.launch {
// 子协程2
}
在这个例子中,如果 子协程1失败了,scope将不会被取消,子协程2也不会被取消。
另一个例子:
val scope = CoroutineScope(Job())
scope.launch {
supervisorScope {
launch {
// 子协程1
}
launch {
// 子协程2
}
}
}
在这个例子中,由于 supervisorScope创建了一个使用 SupervisorJob的子 scope,如果 子协程1失败了,子协程2将不会被取消。相反,如果你在该实现中使用 coroutineScope,那么失败将被传播并且整个 scope都将被取消。
测试题!父节点是谁?🎯
给出下面的代码片段,你能确定 子协程1的父节点是哪种 Job类型吗?
val scope = CoroutineScope(Job())
scope.launch(SupervisorJob()) {
//新协程
launch {
// 子协程1
}
launch {
// 子协程2
}
}
子协程1的 parent Job是 Job类型!希望你答对了!虽然第一眼看上去,你可能会觉得是一个 SupervisorJob,然而并不是,因为一个新的协程总是会被赋予一个新的 Job(),它在这个例子中覆盖了 SupervisorJob。SupervisorJob是通过 scope.launch创建的协程的 parent job;因此,可以说 SupervisorJob在这段代码中没起任何作用。

因此,如果 子协程1或者 子协程2失败,失败会到达 scope,所有在这个 scope中开始的工作都将被取消。
记住
SupervisorJob只有在其作为scope的一部分时才起作用,要么使用supervisorScope创建,要么使用CoroutineScope(SuperVisorJob())创建。
把 SupervisorJob传递给协程构建器作为参数,不会产生你认为的取消效果。
关于异常,如何任何一个子协程抛出异常,SupervisorJob不会在层次结构中传播该异常,而会让协程来处理它。
底层实现
如果你好奇 Job在底层是如何工作的,可以查看 JobSupport.kt文件中的 childCancelled方法和 notifyCancelling方法。
在 SupervisorJob的实现中,childCancelled方法只返回 false,意味着它不会传播取消,也不会处理异常。
处理异常 👩🚒
协程使用Kotlin的常规语法来处理异常:try/catch,或者内建的帮助方法像 runCatching(其内部也是用 try/catch)。
我们前面说过未捕获的异常总是会被抛出,然而,不同的协程构建器处理异常的方式不一样。
Launch
使用 launch,异常一发生立马就会被抛出。因此,你可以用 try/catch把可能发生异常的代码包住,如下:
scope.launch {
try {
codeThatCanThrowExceptions()
} catch(e: Exception) {
// Handle exception
}
}
使用
launch,异常一发生立马就会被抛出。
Async
当 async作为 根协程(CoroutineScope或者 supervisorScope的直接子协程),异常不会自动抛出,相反,当你调用 .await()方法时它们才被抛出。
要处理 async作为根协程时里面抛出的异常,你可以把 .await()调用包裹到 try/catch里面:
supervisorScope {
val deferred = async {
codeThatCanThrowExceptions()
}
try {
deferred.await()
} catch(e: Exception) {
// Handle exception thrown in async
}
}
在这个例子中,调用 async永远不会抛异常,所以不必包裹它。await会抛出 async协程中发生的异常。
当
async作为根协程,调用.await的时候异常才会被抛出。
也要注意我们是在 supervisorScope中调用 async和 await。我们前面说过,SupervisorJob会让协程自己处理异常;而 Job会自动向上传播异常,因此 catch代码块将不会被调用:
coroutineScope {
try {
val deferred = async {
codeThatCanThrowExceptions()
}
deferred.await()
} catch(e: Exception) {
// async 中发生的异常不会在这里被捕获
// 异常会传给scope
}
}
而且,在协程中创建的协程,不管其协程构建器是怎样的,其中的异常总是会传播。例如:
val scope = CoroutineScope(Job())
scope.launch {
async {
// 如果asyn发生异常,launch将会抛异常,不管有没有调用.awiat()
}
}
在这个例子中,如果 async发生了异常,它将立马被抛出,因为 scope的直接子协程是 launch。原因是 async(其 CoroutineContext里包含的是 Job)会自动把异常传播给其父节点(launch),然后抛出异常。
⚠️
coroutineScope构建器中抛出的异常,或由协程创建的协程中抛出的异常,不会被try/catch捕获!
在 SupervisorJob章节中,我们提到了 CoroutineExceptionHandler。让我们深入了解一下吧!
CoroutineExceptionHandler
CoroutineExceptionHandler是 CoroutineContext的一个可选元素,用来处理未被捕获的异常。
下面是如何定义一个 CoroutineExceptionHandler,当一个异常被捕获,你可以获取发生异常所在的 CoroutineContext的信息,以及异常本身:
val handler = CoroutineExceptionHandler { context, exception ->
println("Caught $exception")
}
如果 CoroutineExceptionHandler满足以下条件,异常就会被捕获:
- 当 ⏰:异常是被协程自动抛出的(
handler配合launch使用,而不是async) - 在 🌍:如果
handler存在于在CoroutineScope或者根协程(CoroutineScope或supervisorScope的直接子协程)的CoroutineContext。
让我们使用上面定义的 CoroutineExceptionHandler来看一些例子。在下面这个例子中,异常会被 handler捕获:
val scope = CoroutineScope(Job())
scope.launch(handler) {
launch {
throw Exception("Failed coroutine")
}
}
下面这个例子,handler初始化于内部协程,异常不会被捕获:
val scope = CoroutineScope(Job())
scope.launch {
launch(handler) {
throw Exception("Failed coroutine")
}
}
这个异常不会被捕获是因为 handler没有初始化于正确的 CoroutineContext。在内部的 launch中,异常一旦发生就会立马向上传播到父节点,由于父节点并不知道 handler,所以就会被抛出。
应用中合理的异常处理对于良好的用户体验非常重要,即使事情没有按照预期发展。
当异常发生的时候,如果想避免取消的传播,记得使用 SupervisorJob;相反,则使用 Job。
未捕获的异常将被传播,捕获它们以提供好的用户体验!
本文介绍了Kotlin协程中的异常处理,包括SupervisorJob的作用,如何防止异常导致的协程取消,以及Launch、Async和CoroutineExceptionHandler在异常处理中的不同行为。强调了合理处理异常对提升用户体验的重要性。
774

被折叠的 条评论
为什么被折叠?



