异常处理

  本节内容涵盖了异常处理与在异常上取消。我们已经知道取消协程会在挂起点抛出 CancellationException 并且它会被协程的机制所忽略。在这⾥我们会看看在取消过程中抛出异常或同 ⼀个协程的多个⼦协程抛出异常时会发⽣什么。

异常的传播

  协程构建器有两种形式:⾃动传播异常(launch 与 actor)或向⽤⼾暴露异常(async 与 produce)。当 这些构建器⽤于创建⼀个根协程时,即该协程不是另⼀个协程的⼦协程,前者这类构建器将异常视为未 捕获异常,类似 Java 的 Thread.uncaughtExceptionHandler,⽽后者则依赖⽤⼾来最终消费异 常,例如通过 await 或 receive(produce 与 receive 的相关内容包含于通道章节)。 可以通过⼀个使⽤ GlobalScope 创建根协程的简单⽰例来进⾏演⽰:

import kotlinx.coroutines.*
fun main() = runBlocking {
val job = GlobalScope.launch { // launch 根协程
println("Throwing exception from launch")
throw IndexOutOfBoundsException() // 我们将在控制台打印
Thread.defaultUncaughtExceptionHandler
}
job.join()
println("Joined failed job")
val deferred = GlobalScope.async { // async 根协程
println("Throwing exception from async")
throw ArithmeticException() // 没有打印任何东西,依赖⽤⼾去调⽤等待
}
try {
deferred.await()
println("Unreached")
} catch (e: ArithmeticException) {
println("Caught ArithmeticException")
}
}

  这段代码的输出如下(调试):

Throwing exception from launch
Exception in thread "DefaultDispatcher-worker-2 @coroutine#2"
java.lang.IndexOutOfBoundsException
Joined failed job
Throwing exception from async
Caught ArithmeticException

  

CoroutineExceptionHandler

  It is possible to customize the default behavior of printing uncaught exceptions to the console. CoroutineExceptionHandler context element on a root coroutine can be used as generic catch block for this root coroutine and all its children where custom exception handling may take place. It is similar to Thread.uncaughtExceptionHandler. You cannot recover from the exception in the CoroutineExceptionHandler . The coroutine had already completed with the corresponding exception when the handler is called. Normally, the handler is used to log the exception, show some kind of error message, terminate, and/or restart the application. 在 JVM 中可以重定义⼀个全局的异常处理者来将所有的协程通过 ServiceLoader 注册到 CoroutineExceptionHandler。全局异常处理者就如同 Thread.defaultUncaughtExceptionHandler ⼀样,在没有更多的指定的异常处理者被注册的 时候被使⽤。在 Android 中,uncaughtExceptionPreHandler 被设置在全局协程异常处理者中。

  CoroutineExceptionHandler is invoked only on uncaught exceptions — exceptions that were not handled in any other way. In particular, all children coroutines (coroutines created in the context of another Job) delegate handling of their exceptions to their parent coroutine, which also delegates to the parent, and so on until the root, so the CoroutineExceptionHandler installed in their context is never used. In addition to that, async builder always catches all exceptions and represents them in the resulting Deferred object, so its CoroutineExceptionHandler has no effect either

Coroutines running in supervision scope do not propagate exceptions to their parent and are excluded from this rule. A further Supervision section of this document gives more details.

  

val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
val job = GlobalScope.launch(handler) { // root coroutine, running in GlobalScope
throw AssertionError()
}
val deferred = GlobalScope.async(handler) { // also root, but async instead of launch
throw ArithmeticException() // 没有打印任何东西,依赖⽤⼾去调⽤ deferred.await()
}
joinAll(job, deferred)

  这段代码的输出如下:

CoroutineExceptionHandler got java.lang.AssertionError

  

取消与异常

  取消与异常紧密相关。协程内部使⽤ CancellationException 来进⾏取消,这个异常会被所有的 处理者忽略,所以那些可以被 catch 代码块捕获的异常仅仅应该被⽤来作为额外调试信息的资源。当 ⼀个协程使⽤ Job.cancel 取消的时候,它会被终⽌,但是它不会取消它的⽗协程。

val job = launch {
val child = launch {
try {
delay(Long.MAX_VALUE)
} finally {
println("Child is cancelled")
}
}
yield()
println("Cancelling child")
child.cancel()
child.join()
yield()
println("Parent is not cancelled")
}
job.join()

  这段代码的输出如下:

Cancelling child
Child is cancelled
Parent is not cancelled

  如果⼀个协程遇到了 CancellationException 以外的异常,它将使⽤该异常取消它的⽗协程。这 个⾏为⽆法被覆盖,并且⽤于为结构化的并发(structured concurrency)提供稳定的协程层级结构。 CoroutineExceptionHandler 的实现并不是⽤于⼦协程。

在本例中,CoroutineExceptionHandler 总是被设置在由 GlobalScope 启动的协程中。将异常处理者设置在 runBlocking 主作⽤域内启动的协程中是没有意义的,尽管⼦协程已经设置了异常处理者,但是主协程也总是会被取消的。

  当⽗协程的所有⼦协程都结束后,原始的异常才会被⽗协程处理,⻅下⾯这个例⼦

val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
val job = GlobalScope.launch(handler) {
launch { // 第⼀个⼦协程
try {
delay(Long.MAX_VALUE)
} finally {
withContext(NonCancellable) {
println("Children are cancelled, but exception is not handled until all
children terminate")
delay(100)
println("The first child finished its non cancellable block")
}
}
}
launch { // 第⼆个⼦协程
delay(10)
println("Second child throws an exception")
throw ArithmeticException()
}
}
job.join()

  这段代码的输出如下:

Second child throws an exception 
Children are cancelled, but exception is not handled until all children terminate
The first child finished its non cancellable block
CoroutineExceptionHandler got java.lang.ArithmeticException

  

异常聚合

  当协程的多个⼦协程因异常⽽失败时,⼀般规则是“取第⼀个异常”,因此将处理第⼀个异常。在第⼀个 异常之后发⽣的所有其他异常都作为被抑制的异常绑定⾄第⼀个异常。

import kotlinx.coroutines.*
import java.io.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception with suppressed
${exception.suppressed.contentToString()}")
}
val job = GlobalScope.launch(handler) {
launch {
try {
delay(Long.MAX_VALUE) // 当另⼀个同级的协程因 IOException 失败时,它将被取消
} finally {
throw ArithmeticException() // 第⼆个异常
}
}
launch {
delay(100)
throw IOException() // ⾸个异常
}
delay(Long.MAX_VALUE)
}
job.join()
}

  这段代码的输出如下:

CoroutineExceptionHandler got java.io.IOException with suppressed
[java.lang.ArithmeticException]

  取消异常是透明的,默认情况下是未包装的:

val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
val job = GlobalScope.launch(handler) {
val inner = launch { // 该栈内的协程都将被取消
launch {
launch {
throw IOException() // 原始异常
}
}
}
try {
inner.join()
} catch (e: CancellationException) {
println("Rethrowing CancellationException with original cause")
throw e // 取消异常被重新抛出,但原始 IOException 得到了处理
}
}
job.join()

  这段代码的输出如下:

Rethrowing CancellationException with original cause
CoroutineExceptionHandler got java.io.IOException

  

监督

  正如我们之前研究的那样,取消是在协程的整个层次结构中传播的双向关系。让我们看⼀下需要单向取消的情况。

  此类需求的⼀个良好⽰例是在其作⽤域内定义作业的 UI 组件。如果任何⼀个 UI 的⼦作业执⾏失败了, 它并不总是有必要取消(有效地杀死)整个 UI 组件,但是如果 UI 组件被销毁了(并且它的作业也被取消 了),由于它的结果不再被需要了,它有必要使所有的⼦作业执⾏失败。

  另⼀个例⼦是服务进程孵化了⼀些⼦作业并且需要 监督 它们的执⾏,追踪它们的故障并在这些⼦作业 执⾏失败的时候重启。

监督作业

  SupervisorJob 可以被⽤于这些⽬的。它类似于常规的 Job,唯⼀的不同是:SupervisorJob 的取消只 会向下传播。这是⾮常容易从⽰例中观察到的:

import kotlinx.coroutines.*
fun main() = runBlocking {
val supervisor = SupervisorJob()
with(CoroutineScope(coroutineContext + supervisor)) {
// 启动第⼀个⼦作业——这个⽰例将会忽略它的异常(不要在实践中这么做!)
val firstChild = launch(CoroutineExceptionHandler { _, _ -> }) {
println("First child is failing")
throw AssertionError("First child is cancelled")
}
// 启动第两个⼦作业
val secondChild = launch {
firstChild.join()
// 取消了第⼀个⼦作业且没有传播给第⼆个⼦作业
println("First child is cancelled: ${firstChild.isCancelled}, but second one
is still active")
try {
delay(Long.MAX_VALUE)
} finally {
// 但是取消了监督的传播
println("Second child is cancelled because supervisor is cancelled")
}
}
// 等待直到第⼀个⼦作业失败且执⾏完成
firstChild.join()
println("Cancelling supervisor")
supervisor.cancel()
secondChild.join()
}
}

  这段代码的输出如下:

First child is failing
First child is cancelled: true, but second one is still active
Cancelling supervisor
Second child is cancelled because supervisor is cancelled

  

监督作用域

  对于作⽤域的并发,supervisorScope 可以被⽤来替代 coroutineScope 来实现相同的⽬的。它只会单 向的传播并且当作业⾃⾝执⾏失败的时候将所有⼦作业全部取消。作业⾃⾝也会在所有的⼦作业执⾏ 结束前等待,就像 coroutineScope 所做的那样。

import kotlin.coroutines.*
import kotlinx.coroutines.*
fun main() = runBlocking {
try {
supervisorScope {
val child = launch {
try {
println("Child is sleeping")
delay(Long.MAX_VALUE)
} finally {
println("Child is cancelled")
}
}
// 使⽤ yield 来给我们的⼦作业⼀个机会来执⾏打印
yield()
println("Throwing exception from scope")
throw AssertionError()
}
} catch(e: AssertionError) {
println("Caught assertion error")
}
}

  这段代码的输出如下:

Child is sleeping
Throwing exception from scope
Child is cancelled
Caught assertion error

  

监督协程中的异常

  常规的作业和监督作业之间的另⼀个重要区别是异常处理。监督协程中的每⼀个⼦作业应该通过异常 处理机制处理⾃⾝的异常。这种差异来⾃于⼦作业的执⾏失败不会传播给它的⽗作业的事实。这意味 着在 supervisorScope 内部直接启动的协程确实使⽤了设置在它们作⽤域内的 CoroutineExceptionHandler,与⽗协程的⽅式相同(查看 CoroutineExceptionHandler ⼩节以获知 更多细节)。

import kotlin.coroutines.*
import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
supervisorScope {
val child = launch(handler) {
println("Child throws an exception")
throw AssertionError()
}
println("Scope is completing")
}
println("Scope is completed")
}

  这段代码的输出如下:

Scope is completing
Child throws an exception
CoroutineExceptionHandler got java.lang.AssertionError
Scope is completed

  

kotlin协程——>异常处理的更多相关文章

  1. Kotlin 协程一 —— 全面了解 Kotlin 协程

    一.协程的一些前置知识 1.1 进程和线程 1.1.1基本定义 1.1.2为什么要有线程 1.1.3 进程与线程的区别 1.2 协作式与抢占式 1.2.1 协作式 1.2.2 抢占式 1.3 协程 二 ...

  2. Kotlin协程解析系列(上):协程调度与挂起

    vivo 互联网客户端团队- Ruan Wen 本文是Kotlin协程解析系列文章的开篇,主要介绍Kotlin协程的创建.协程调度与协程挂起相关的内容 一.协程引入 Kotlin 中引入 Corout ...

  3. Kotlin协程第一个示例剖析及Kotlin线程使用技巧

    Kotlin协程第一个示例剖析: 上一次https://www.cnblogs.com/webor2006/p/11712521.html已经对Kotlin中的协程有了理论化的了解了,这次则用代码来直 ...

  4. Retrofit使用Kotlin协程发送请求

    Retrofit2.6开始增加了对Kotlin协程的支持,可以通过suspend函数进行异步调用.本文简单介绍一下Retrofit中协程的使用 导入依赖 app的build文件中加入: impleme ...

  5. Kotlin协程基础

    开发环境 IntelliJ IDEA 2021.2.2 (Community Edition) Kotlin: 212-1.5.10-release-IJ5284.40 我们已经通过第一个例子学会了启 ...

  6. Android Kotlin协程入门

    Android官方推荐使用协程来处理异步问题.以下是协程的特点: 轻量:单个线程上可运行多个协程.协程支持挂起,不会使正在运行协程的线程阻塞.挂起比阻塞节省内存,且支持多个并行操作. 内存泄漏更少:使 ...

  7. rxjava回调地狱-kotlin协程来帮忙

    本文探讨的是在tomcat服务端接口编程中, 异步servlet场景下( 参考我另外一个文章),用rxjava来改造接口为全流程异步方式 好处不用说 tomcat的worker线程利用率大幅提高,接口 ...

  8. Kotlin协程通信机制: Channel

    Coroutines Channels Java中的多线程通信, 总会涉及到共享状态(shared mutable state)的读写, 有同步, 死锁等问题要处理. 协程中的Channel用于协程间 ...

  9. Kotlin协程作用域与Job详解

    Job详解: 在上一次https://www.cnblogs.com/webor2006/p/11725866.html中抛出了一个问题: 所以咱们将delay去掉,需要改造一下,先把主线程的dela ...

  10. Kotlin协程作用域与构建器详解

    在上次我们是通过了这种方式来创建了一个协程: 接着再来看另一种创建协程的方式: 下面用它来实现上一次程序一样的效果,先来回顾一下上一次程序的代码: 好,下面改用runBlocking的方式: 运行一下 ...

随机推荐

  1. 【C3】02 操作总览

    在这篇文章中,我们将会拿一个简单的HTML文档做例子,并且在上边使用CSS样式,期待你能在此过程中学会更多有关CSS的实战性知识. 前置知识 在开始本单元之前,您应该: 基本熟悉计算机操作. 基本工作 ...

  2. 【Node】下载安装(Linux)

    不要使用源码包安装!!!编译时间太长!! 不要使用源码包安装!!!编译时间太长!! 不要使用源码包安装!!!编译时间太长!! 使用Node源码包安装 这里使用的是源码包安装 Node官网地址:也不是官 ...

  3. AI大模型的技术之母 —— Attention Is All You Need —— Tansformer

    论文地址: https://arxiv.org/abs/1706.03762

  4. python绘图库matplotlib:刻度线的方向调整, in, out, inout

    前文相关: python绘图库matplotlib:画线的标志marker的设置--类型/size/空心/边线颜色及大小/显示marker超出边界部分 由于工作需要经常用matplotlib来绘图,但 ...

  5. Functional PHP (通义千问)

    Functional PHP 是一个专为 PHP 开发者设计的库,旨在引入函数式编程的概念和工具,帮助开发者编写更高效.可读性强的代码.以下是几个使用 Functional PHP 库进行函数式编程的 ...

  6. 图片热区。vue3+ts和vue3+js写法(js没写完数据,功能完善)

    废话不多说,上代码 vue3+ts <!-- 热区组件 --> <template> <el-dialog v-model="dialog_visible&qu ...

  7. 9组-Beta冲刺-5/5

    一.基本情况(15分) 队名:不行就摆了吧 组长博客:9组-Beta冲刺-5/5 GitHub链接:https://github.com/miaohengming/studynote/tree/mai ...

  8. disconf分布式配置管理(一) 安装与配置

    一.背景 在生产部署过程中,遇到以下问题: 1.由于节点较多,每次增量修改配置文件后都需要每个节点替换配置文件. 2.有些动态配置修改后,需要重启服务. 二.解决方案 1.使用linux文件共享配置文 ...

  9. Windows 7远程桌面连接Ubuntu 18.04

    从Windows 7远程到Windows系统比较简单,只要对方电脑开启远程桌面功能就可以了,但Windows 7远程桌面连接到Ubuntu 16.04比较复杂一点,具体操作步骤如下. 1 安装xrdp ...

  10. Graphics2D绘图方法总结

    一.简介 在开发中可能会遇到这样一类场景,业务复杂度不算太高,技术难度不算太深,但是做起来就很容易把人整破防,伤害很高侮辱性很强的:绘图. 绘图最怕有人挑刺:这里变形,那里不对,全图失真. 最近在处理 ...