协程06 - 异常处理

在协程中,异常的传播形式有两种:

  • 一种是自动传播( launchactor)
  • 一种是向用户暴露该异常( asyncproduce )

这两种的区别在于,前者的异常传递过程是层层向上传递(如果异常没有被捕获),而后者将不会向上传递,会在调用处直接暴漏。

CoroutineExceptionHandler

其是用于在协程中全局捕获异常行为的最后一种机制,你可以理解为,类似 [Thread.uncaughtExceptionHandler] 一样。但需要注意的是,CoroutineExceptionHandler 仅在未捕获的异常上调用。

当子协程发生异常时,它会优先将异常委托给父协程区处理,以此类推直到根协程作用域或者顶级协程 。因此其永远不会使用我们子协程 CoroutineContext 传递的 CoroutineExceptionHandler

如下示例所示:

scss 复制代码
val scope = CoroutineScope(Job())
 scope.launch() {
     launch(CoroutineExceptionHandler { _, _ -> }) {
         delay(10)
         throw RuntimeException()
     }
 }

运行时,仍然会抛出异常。原因就是我们的 CoroutineExceptionHandler 位置不是根协程。

再看一个例子:

kotlin 复制代码
@OptIn(DelicateCoroutinesApi::class)
@JvmStatic
fun main(args: Array<String>) = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception")
    }
    val deferred = GlobalScope.async(handler) {
        throw ArithmeticException()
    }
    deferred.await()
}

这里我们使用了 async,它会直接抛出异常给开发者处理,而不是进行传递。所以这里会报错。

其根本原因在于,async 创建的协程类型是 DeferredCoroutine 类型,它不处理异常。而 launch 创建的是 StandaloneCoroutine 类型,它处理异常:

kotlin 复制代码
private open class StandaloneCoroutine(
    parentContext: CoroutineContext,
    active: Boolean
) : AbstractCoroutine<Unit>(parentContext, initParentJob = true, active = active) {
    override fun handleJobException(exception: Throwable): Boolean {
        handleCoroutineException(context, exception)
        return true
    }
}

可以看到,它复写了 handleJobException 方法,而DeferredCoroutine 没有。

父协程处理异常时机

只有当所有子运行程序都终止时,父运行程序才会处理异常,下面的例子就是证明:

scss 复制代码
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception -> 
        println("CoroutineExceptionHandler got $exception") 
    }
    val job = GlobalScope.launch(handler) {
        launch { // the first child
            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 { // the second child
            delay(10)
            println("Second child throws an exception")
            throw ArithmeticException()
        }
    }
    job.join()
}

第二个协程出现异常,父协程会取消其他子协程,当第一个子协程取消的时候,它进入 finally,我们在 finally 里面做了一个延迟操作。

父协程处理异常的时机,是在这个子协程延迟动作完成之后,故输出为:

sql 复制代码
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

异常合并

当一个协程的多个子协程出现异常失败时,一般的规则是 "第一个异常胜出",因此第一个异常会得到处理。在第一个异常之后发生的所有其他异常都会作为被抑制的异常附加到第一个异常上。

kotlin 复制代码
@OptIn(DelicateCoroutinesApi::class)
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) // it gets cancelled when another sibling fails with IOException
            } finally {
                throw ArithmeticException() // the second exception
            }
        }
        launch {
            delay(100)
            throw IOException() // the first exception
        }
        delay(Long.MAX_VALUE)
    }
    job.join()  
}

输出为:

csharp 复制代码
CoroutineExceptionHandler got java.io.IOException with suppressed [java.lang.ArithmeticException]

但是 CancellationException 不会被附加上去,它是透明的。

Supervision job

假设我们在协程的作用域中定义了任务的用户界面组件。如果 UI 的任何子任务失败,并不一定需要取消(实际上是杀死)整个 UI 组件,但如果 UI 组件被销毁(其任务被取消),则有必要取消所有子任务。

这种情况下,我们就需要防止子协程被取消,那么怎么才能做到呢?

SupervisorJob 可用于上述目的。

scss 复制代码
fun main() = runBlocking {
    val supervisor = SupervisorJob()
    with(CoroutineScope(coroutineContext + supervisor)) {
        // launch the first child -- its exception is ignored for this example (don't do this in practice!)
        val firstChild = launch(CoroutineExceptionHandler { _, _ ->  }) {
            println("The first child is failing")
            throw AssertionError("The first child is cancelled")
        }
        // launch the second child
        val secondChild = launch {
            firstChild.join()
            // Cancellation of the first child is not propagated to the second child
            println("The first child is cancelled: ${firstChild.isCancelled}, but the second one is still active")
            try {
                delay(Long.MAX_VALUE)
            } finally {
                // But cancellation of the supervisor is propagated
                println("The second child is cancelled because the supervisor was cancelled")
            }
        }
        // wait until the first child fails & completes
        firstChild.join()
        println("Cancelling the supervisor")
        supervisor.cancel()
        secondChild.join()
    }
}

当 firstChild 协程出现异常时,它跑到了自己的 CoroutineExceptionHandler 里面去处理了。这是因为它从父协程那里继承了 SupervisorJob,所以它不会向上传播遗产。而且,firstChild 的失败不会影响 secondChild 的运行。

输出:

sql 复制代码
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
相关推荐
我要洋人死18 分钟前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人30 分钟前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人30 分钟前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR36 分钟前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香38 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q24985969341 分钟前
前端预览word、excel、ppt
前端·word·excel
小华同学ai1 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书
Gavin_9151 小时前
【JavaScript】模块化开发
前端·javascript·vue.js
懒大王爱吃狼2 小时前
Python教程:python枚举类定义和使用
开发语言·前端·javascript·python·python基础·python编程·python书籍
逐·風6 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#