Kotlin协程的异常处理(九)

一、前言

​ 协程在使用过程中会有异常情况,我么需要对异常进行处理。协程构建器有两种形式:自动传播异常(launchactor)或向用户暴露异常(asyncproduce)。 当这些构建器用于创建一个根协程时,即该协程不是另一个协程的协程, 前者这类构建器将异常视为未捕获异常,类似 Java 的 Thread.uncaughtExceptionHandler, 而后者则依赖用户来最终消费异常,例如通过 awaitreceiveproducereceive 的相关内容包含于通道章节)。

二、异常传播

​ 这里通过一个简单的例子对异常情况进行演示

@Test
fun exceptionTest(){
    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")
        }
    }
}

可以看出其中async的异常可以在 await是进行捕获,而launch的异常是无法在join时候进行捕获的。

三、CoroutineExceptionHandler

针对于那些未捕获的异常该怎么做呢?官方提供了CoroutineExceptionHandler方式,该方式可以定义一个全局的协程异常处理方式来对所有未捕获的异常进行处理,因为async的异常被deferred进行捕获,所以无法使用这种方式,已经被自己处理的异常也不能使用这种方式。演示程序如下

val handler = CoroutineExceptionHandler { _, exception -> 
    println("CoroutineExceptionHandler got $exception") 
}
val job = GlobalScope.launch(handler) { // 根协程,运行在 GlobalScope 中
    throw AssertionError()
}
val deferred = GlobalScope.async(handler) { // 同样是根协程,但使用 async 代替了 launch
    throw ArithmeticException() // 没有打印任何东西,依赖用户去调用 deferred.await()
}
joinAll(job, deferred)

打印结果如下

CoroutineExceptionHandler got java.lang.AssertionError

四、取消与异常

取消与异常紧密相连。协程内部使用 CancellationException 来进行取消,这个异常会被所有的处理者忽略,所以那些可以被 catch 代码块捕获的异常仅仅应该被用来作为额外调试信息的资源。 当一个协程使用 Job.cancel 取消的时候,它会被终止,但是它不会取消它的父协程。如果一个协程遇到了 CancellationException 以外的异常,它将使用该异常取消它的父协程。 这个行为无法被覆盖,并且用于为结构化的并发(structured concurrency) 提供稳定的协程层级结构。当父协程的所有子协程都结束后,原始的异常才会被父协程处理。参考下面的例子

@Test
fun exceptionTest1(){
    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()
    }
}

可以看出当第二个协程在10ms后出现异常时并没有及时的被捕获,而是在100ms后的第一个协程执行完毕后这个异常才被CoroutineExceptionHandler捕获。

五、异常的聚合

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

@Test
fun exceptionPolymerization(){
    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]

六、取消异常

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

@Test
fun exceptionCancel(){
    runBlocking {
        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

因为异常导致父协程取消,而取消需要使用CancellationException,所以触发取消异常。因为CancellationException不会被CoroutineExceptionHandler捕获,所以最终CoroutineExceptionHandler里面只会打印IOException。如果想对CancellationException进行捕获就要使用try{}catch{}inner.join()进行处理。

七、监督

此类需求的一个良好示例是在其作用域内定义作业的 UI 组件。如果任何一个 UI 的子作业执行失败了,它并不总是有必要取消(有效地杀死)整个 UI 组件, 但是如果 UI 组件被销毁了(并且它的作业也被取消了),由于它的结果不再被需要了,它有必要使所有的子作业执行失败。SupervisorJob 可以用于这些目的。 它类似于常规的 Job,唯一的不同是:SupervisorJob 的取消只会向下传播。这是很容易用以下示例演示:

@Test
fun supervisorJobTest() = runBlocking {
    val supervisor = SupervisorJob()
    with(CoroutineScope(coroutineContext + supervisor)) {
        // 启动第一个子作业——这个示例将会忽略它的异常(不要在实践中这么做!)
        val firstChild = launch(CoroutineExceptionHandler { _, _ ->  }) {
            println("The first child is failing")
            throw AssertionError("The first child is cancelled")
        }
        // 启动第二个子作业
        val secondChild = launch {
            firstChild.join()
            // 取消了第一个子作业且没有传播给第二个子作业
            println("The first child is cancelled: ${firstChild.isCancelled}, but the second one is still active")
            try {
                delay(Long.MAX_VALUE)
            } finally {
                // 但是取消了监督的传播
                println("The second child is cancelled because the supervisor was cancelled")
            }
        }
        // 等待直到第一个子作业失败且执行完成
        firstChild.join()
        println("Cancelling the supervisor")
        supervisor.cancel()
        secondChild.join()
    }
}

运行结果如下

The first child is failing
The first child is cancelled: true, but the second one is still active
Cancelling the supervisor
The second child is cancelled because the supervisor was cancelled

可以看到第一个异常的取消并没有导致第二个异常的取消。但是如果将SupervisorJob()改为Job(),则会出现以下情况

The first child is failing
Cancelling the supervisor

随着第一个协程的取消,第二个协程也随之取消了。

八、监督作用域

​ 对于作用域的并发,可以用 supervisorScope 来替代 coroutineScope 来实现相同的目的。它只会单向的传播并且当作业自身执行失败的时候将所有子作业全部取消。作业自身也会在所有的子作业执行结束前等待, 就像 coroutineScope 所做的那样。

@Test
fun supervisorScopeTest() = runBlocking {
    try {
        supervisorScope {
            val child = launch {
                try {
                    println("The child is sleeping")
                    delay(Long.MAX_VALUE)
                } finally {
                    println("The child is cancelled")
                }
            }
            // 使用 yield 来给我们的子作业一个机会来执行打印
            yield()//暂时挂起,让其它耗时操作去执行
            println("Throwing an exception from the scope")
            throw AssertionError()
        }
    } catch(e: AssertionError) {
        println("Caught an assertion error")
    }
}

运行结果如下

The child is sleeping
Throwing an exception from the scope
The child is cancelled
Caught an assertion error
  • coroutineScope 内部的异常会向上传播,子协程未捕获的异常会向上传递给父协程,任何一个子协程异常退出,会导致整体的退出。
  • supervisorScope 内部的异常不会向上传播,一个子协程异常退出,不会影响父协程和兄弟协程的运行。

九、监督协程中的异常

Job和SupervisorJob之间的另一个重要区别是异常处理。 监督协程中的每一个子作业应该通过异常处理机制处理自身的异常。 这种差异来自于子作业的执行失败不会传播给它的父作业的事实。 这意味着在 supervisorScope 内部直接启动的协程确实使用了设置在它们作用域内的 CoroutineExceptionHandler,与父协程的方式相同 (参见 CoroutineExceptionHandler 小节以获知更多细节)。

@Test
fun supervisorTest() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception")
    }
    supervisorScope {
        val child = launch(handler) {
            println("The child throws an exception")
            throw AssertionError()
        }
        println("The scope is completing")
    }
    println("The scope is completed")
}

结果如下

The scope is completing
The child throws an exception
CoroutineExceptionHandler got java.lang.AssertionError
The scope is completed

如果将supervisorScope换成coroutineScope,程序则会出现异常。无法捕获,除非自己定义一个val scrop = CoroutineScope(Dispatchers.Default+ Job())

十、参考链接

  1. 异常处理和监督

    https://www.kotlincn.net/docs/reference/coroutines/exception-handling.html

  2. coroutineScope , supervisorScope 和 launch ,async 的混合异常处理

    https://juejin.cn/post/6844904190569873422

  3. 揭秘kotlin协程中的CoroutineContext

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值