公众号:字节数组
希望对你有所帮助 🤣🤣
最近一直在了解关于kotlin协程的知识,那最好的学习资料自然是官方提供的学习文档了,看了看后我就萌生了翻译官方文档的想法。前后花了要接近一个月时间,一共九篇文章,在这里也分享出来,希望对读者有所帮助。个人知识所限,有些翻译得不是太顺畅,也希望读者能提出意见
协程官方文档:coroutines-guide
协程官方文档中文翻译:coroutines-cn-guide
本节讨论协程关于异常的处理和取消异常。我们已经知道,取消协程会使得在挂起点抛出 CancellationException,而协程机制会忽略这个异常。但是,如果在取消期间抛出异常,或者协程的多个子协程抛出异常,此时会发生什么情况呢?
一、异常的传播
协程构建器有两种类型:自动传播异常(launch 和 actor)和向用户公开异常(async 和 product)。前者将异常视为未捕获异常,类似于 Java 的 Thread.uncaughtExceptionHandler,而后者则需要由开发者自己来处理最终的异常,例如通过 await 或 receive(product 和 receive 在 Channels 章节介绍)
可以通过在 GlobalScope 创建协程的简单示例来演示:
1 | kotlin复制代码import kotlinx.coroutines.* |
运行结果:
1 | kotlin复制代码Throwing exception from launch |
二、CoroutineExceptionHandler
如果不想将所有的异常都打印到控制台上,CoroutineExceptionHandler 上下文元素可以作为协程全局通用的 catch 块,在这里进行自定义日志记录或异常处理。它类似于对线程使用 Thread.uncaughtExceptionHandler
在 JVM 上,可以通过 ServiceLoader 注册 CoroutineExceptionHandler 来重新定义所有协程的全局异常处理器。全局异常处理程序类似于 Thread.defaultUncaughtExceptionHandler ,后者在没有注册其它特定处理程序时使用。在 Android 上,uncaughtExceptionPreHandler 作为全局协程异常处理程序存在
CoroutineExceptionHandler 只在预计不会由用户处理的异常上调用,因此在 async 这类协程构造器中注册它没有任何效果
1 | kotlin复制代码import kotlinx.coroutines.* |
运行结果:
1 | kotlin复制代码Caught java.lang.AssertionError |
三、取消和异常
取消和异常是紧密联系的。协程在内部使用 CancellationException 来进行取消,所有处理程序都会忽略这类异常,因此它们仅用作调试信息的额外来源,这些信息可以用 catch 块捕获。当使用 Job.cancel 取消协程时,协程将停止运行,但不会取消其父协程
1 | kotlin复制代码import kotlinx.coroutines.* |
运行结果:
1 | kotlin复制代码Cancelling child |
如果协程遇到 CancellationException 以外的异常,它将用该异常取消其父级。无法重写此行为,它用于为不依赖于 CoroutineExceptionHandler 实现的结构化并发,为之提供稳定的协程层次结构。当父级的所有子级终止时,父级将处理原始异常
这也是为什么在这些示例中,总是将 CoroutineExceptionHandler 作为参数传递给在 GlobalScope 中创建的协程中的原因。将 CoroutineExceptionHandler 设置给主 runBlocking 范围内启动的协程是没有意义的,因为尽管设置了异常处理器,主协程在其子级异常抛出后仍将被取消
1 | kotlin复制代码import kotlinx.coroutines.* |
运行结果:
1 | kotlin复制代码Second child throws an exception |
CoroutineExceptionHandler 将等到所有子协程运行结束后再回调。second child 抛出异常后将联动导致 first child 结束运行,之后再将异常交予 CoroutineExceptionHandler 处理
四、异常聚合
如果一个协程的多个子协程抛出异常会发生什么情况呢?一般的规则是“第一个异常会获胜”,因此第一个抛出的异常将传递给异常处理器进行处理,但这也有可能会导致异常丢失。例如,如果在某个协程在抛出异常后,第二个协程在其 finally 块中抛出异常,此时第二个协程的异常将不会传递给 CoroutineExceptionHandler
其中一个解决方案是分别抛出每个异常。await 应该有相同的机制来避免行为不一致,这将导致协程的实现细节(无论它是否将部分工作委托给其子级)泄漏给其异常处理器
1 | kotlin复制代码import kotlinx.coroutines.* |
注意:以上代码只能在支持 suppressed exceptions 的 JDK7+ 版本上正常运行
运行结果:
1 | kotlin复制代码Caught java.io.IOException with suppressed [java.lang.ArithmeticException] |
导致协程停止的异常在默认情况下是会被透传,不会被包装的
1 | kotlin复制代码import kotlinx.coroutines.* |
运行结果:
1 | kotlin复制代码Rethrowing CancellationException with original cause |
即使捕获到了 inner 被取消的异常信息,但最终传递给 CoroutineExceptionHandler 的还是 inner 内部真实的异常信息
五、Supervision
正如我们之前所研究的,取消是一种双向关系,会在整个协程层次结构中传播。但如果需要单向取消呢?
此类需求的一个很好的例子在某个范围内定义了 Job 的 UI 组件。如果 UI 组件的任意一个子任务失败了,此时并不一定需要取消(实际上是终止)整个 UI 组件。但是如果 UI 组件的生命周期结束了(并且取消了它的 Job),那么就必须取消所有子 Job, 因为它们的结果不再是必需的
另一个例子是一个服务器进程,它生成几个子 Job 并且需要监测它们的执行,跟踪它们的失败时机,并且仅重新启动那么失败的子 Job
5.1、SupervisorJob
出于这些目的,可以使用 SupervisorJob。它类似于常规的 Job,唯一的例外是取消操作只向下传播。用一个例子很容易演示:
1 | kotlin复制代码import kotlinx.coroutines.* |
运行结果:
1 | kotlin复制代码First child is failing |
5.2、supervisorScope
对于作用域并发,可以使用 supervisorScope 代替 coroutineScope 来实现相同的目的。它只在一个方向上传播取消操作,并且仅在自身失败时才取消所有子级。它也像 coroutineScope 一样在结束运行之前等待所有的子元素结束运行
1 | kotlin复制代码import kotlin.coroutines.* |
输出结果:
1 | kotlin复制代码Child is sleeping |
以下例子展示了 supervisorScope 中取消操作的单向传播性,子协程的异常不会导致其它子协程取消
1 | kotlin复制代码fun main() = runBlocking { |
运行结果:
1 | kotlin复制代码Child 1 is printing: 1 |
5.3、监督协程中的异常
常规 job 和 supervisor job 的另一个重要区别在于异常处理。每个子级都应该通过异常处理机制自己处理其异常。这种差异来自于这样一个事实:supervisorScope 中子元素的失败不会传导给父级
1 | kotlin复制代码import kotlin.coroutines.* |
运行结果:
1 | kotlin复制代码Scope is completing |
本文转载自: 掘金