协程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
相关推荐
wefly201718 分钟前
m3u8live.cn 在线M3U8播放器,免安装高效验流排错
前端·后端·python·音视频·前端开发工具
C澒1 小时前
微前端容器标准化 —— 公共能力篇:通用打印
前端·架构
德育处主任Pro1 小时前
前端元素转图片,dom-to-image-more入门教程
前端·javascript·vue.js
木斯佳1 小时前
前端八股文面经大全:小红书前端一二面OC(下)·(2026-03-17)·面经深度解析
前端·vue3·proxy·八股·响应式
陈天伟教授1 小时前
人工智能应用- 预测新冠病毒传染性:04. 中国:强力措施遏制疫情
前端·人工智能·安全·xss·csrf
zayzy2 小时前
前端八股总结
开发语言·前端·javascript
今天减肥吗2 小时前
前端面试题
开发语言·前端·javascript
Rabbit_QL2 小时前
【前端UI行话】前端 UI 术语速查表
前端·ui·状态模式
小码哥_常2 小时前
一文带你吃透Android BLE蓝牙开发全流程
前端
小码哥_常2 小时前
从“新老交锋”看Retrofit与Ktor
前端