协程的异常处理3:协程的异常

本文介绍了Kotlin协程中的异常处理,包括SupervisorJob的作用,如何防止异常导致的协程取消,以及Launch、Async和CoroutineExceptionHandler在异常处理中的不同行为。强调了合理处理异常对提升用户体验的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我将翻译三篇介绍协程的 取消异常处理 相关的文章,三篇文章是层层递进的关系。翻译过程中我将尽量忠实于原文。当然,由于水平有限,不能保证完全的翻译正确。如果您发现翻译有错误之处,欢迎在评论里指出。我也将贴出每篇翻译的原文。

这是第三篇。



我们开发者通常花费大量时间打磨应用的正常使用场景。然而,当应用发生意外时,合适的用户体验也同样重要。一方面,对用户来说应用发生崩溃是糟糕的体验;另一方面,当操作执行失败时,给用户提示适当的信息是必不可少的。

合理的异常处理将对用户如何看待你的应用产生很大的影响。在这篇文章中,我们将解释协程中的异常如何传播,以及如何通过多种方式控制它们。



协程突然失败了!怎么办?😱

当一个协程因为异常失败时,它会把异常传播给它的父节点!然后,这个父节点会 1) 取消其余的子协程,2) 取消自己, 3) 把异常传播给自己的父节点。

异常将被传播到层次结构的根节点,所有由这个根节点 CoroutineScope发起的协程都将被取消。

在这里插入图片描述

协程中发生的异常将通过协程的层次结构向上传播

有些情况下这种异常传播是合理的,然而也有些情况不需要这样。试想,有一个与UI相关的 CoroutineScope,用来处理用户交互。如果其中一个子协程抛了异常,那么这个UI scope将被取消,并且整个UI模块将变得无法响应,因为一个被取消的 scope不能再启动协程。

如果你不想要这样的行为该怎么办?作为替代,你可以在创建协程的 CoroutineScopeCoroutineContext中用一个不同的 Job实现,名叫 SuervisorJob


拯救者 SupervisorJob

使用 SupervisorJob,子协程的失败不会影响其余子协程。一个 SupervisorJob不会取消它自己或它的子协程。而且,SupervisorJob也不会传播异常,它会让子协程自己处理异常。

你可以这样创建一个 CoroutineScope:

val uiScope = CoroutineScope(SupervisorJob())

当一个协程失败时,它不会传播取消,如下图所示:

在这里插入图片描述

(一个 SupervisorJob不会因为异常取消自身或其余的子协程)

如果异常没有被处理,并且 CoroutineContext没有 CoroutineExceptionHandler,异常将到达默认线程的 ExceptionHandler。在JVM中,异常将被记录到控制台;在Android中,不管异常发生在哪个线程,都将导致app崩溃。

💥未捕获的异常总会被抛出,不管你使用哪种 Job

同样的行为也适用于 scope构建器 coroutineScopesupervisorScope。这两种构建器将创建一个子 scope(使用 JobSupervisorJob),你可以用它们来组织协程(比如说你想做并行计算,或者你希望它们相互影响or相互不影响)。

提醒SupervisorJob只有在其作为 scope的一部分时才起作用,要么使用 supervisorScope创建,要么使用 CoroutineScope(SuperVisorJob())创建。


Job 还是 SupervisorJob ?🤔

什么时候该使用 Job或者 SupervisorJob?当你不希望失败导致父节点或同级节点被取消时,就使用 SupervisorJob或者 supervisorScope

一个例子:

val scope = CoroutineScope(SupervisorJob())

scope.launch {    
    // 子协程1
}
scope.launch {    
    // 子协程2
}

在这个例子中,如果 子协程1失败了,scope将不会被取消,子协程2也不会被取消。

另一个例子:

val scope = CoroutineScope(Job())

scope.launch {   
    supervisorScope {        
        launch {            
            // 子协程1
        }       
        launch {            
            // 子协程2        
        }    
    }
}

在这个例子中,由于 supervisorScope创建了一个使用 SupervisorJob的子 scope,如果 子协程1失败了,子协程2将不会被取消。相反,如果你在该实现中使用 coroutineScope,那么失败将被传播并且整个 scope都将被取消。


测试题!父节点是谁?🎯

给出下面的代码片段,你能确定 子协程1的父节点是哪种 Job类型吗?


val scope = CoroutineScope(Job())

scope.launch(SupervisorJob()) {
    //新协程
    launch {        
        // 子协程1
    }    
    launch {        
        // 子协程2    
    }
}

子协程1parent JobJob类型!希望你答对了!虽然第一眼看上去,你可能会觉得是一个 SupervisorJob,然而并不是,因为一个新的协程总是会被赋予一个新的 Job(),它在这个例子中覆盖了 SupervisorJobSupervisorJob是通过 scope.launch创建的协程的 parent job;因此,可以说 SupervisorJob在这段代码中没起任何作用。

在这里插入图片描述

因此,如果 子协程1或者 子协程2失败,失败会到达 scope,所有在这个 scope中开始的工作都将被取消。

记住 SupervisorJob只有在其作为 scope的一部分时才起作用,要么使用 supervisorScope创建,要么使用 CoroutineScope(SuperVisorJob())创建。

SupervisorJob传递给协程构建器作为参数,不会产生你认为的取消效果。

关于异常,如何任何一个子协程抛出异常,SupervisorJob不会在层次结构中传播该异常,而会让协程来处理它。


底层实现

如果你好奇 Job在底层是如何工作的,可以查看 JobSupport.kt文件中的 childCancelled方法和 notifyCancelling方法。

SupervisorJob的实现中,childCancelled方法只返回 false,意味着它不会传播取消,也不会处理异常。



处理异常 👩‍🚒

协程使用Kotlin的常规语法来处理异常:try/catch,或者内建的帮助方法像 runCatching(其内部也是用 try/catch)。

我们前面说过未捕获的异常总是会被抛出,然而,不同的协程构建器处理异常的方式不一样。


Launch

使用 launch,异常一发生立马就会被抛出。因此,你可以用 try/catch把可能发生异常的代码包住,如下:

scope.launch {
    try {
        codeThatCanThrowExceptions()
    } catch(e: Exception) {
        // Handle exception
    }
}

使用 launch,异常一发生立马就会被抛出。


Async

async作为 根协程CoroutineScope或者 supervisorScope的直接子协程),异常不会自动抛出,相反,当你调用 .await()方法时它们才被抛出。

要处理 async作为根协程时里面抛出的异常,你可以把 .await()调用包裹到 try/catch里面:

supervisorScope {    
    val deferred = async {        
        codeThatCanThrowExceptions()    
    }    
    
    try {        
        deferred.await()    
    } catch(e: Exception) {        
        // Handle exception thrown in async    
    }
}

在这个例子中,调用 async永远不会抛异常,所以不必包裹它。await会抛出 async协程中发生的异常。

async作为根协程,调用 .await的时候异常才会被抛出。

也要注意我们是在 supervisorScope中调用 asyncawait。我们前面说过,SupervisorJob会让协程自己处理异常;而 Job会自动向上传播异常,因此 catch代码块将不会被调用:

coroutineScope {
    try {
        val deferred = async {
            codeThatCanThrowExceptions()
        }
        deferred.await()
    } catch(e: Exception) {
        // async 中发生的异常不会在这里被捕获 
        // 异常会传给scope
    }
}

而且,在协程中创建的协程,不管其协程构建器是怎样的,其中的异常总是会传播。例如:

val scope = CoroutineScope(Job())
scope.launch {    
    async {        
        // 如果asyn发生异常,launch将会抛异常,不管有没有调用.awiat()
    }
}

在这个例子中,如果 async发生了异常,它将立马被抛出,因为 scope的直接子协程是 launch。原因是 async(其 CoroutineContext里包含的是 Job)会自动把异常传播给其父节点(launch),然后抛出异常。

⚠️coroutineScope构建器中抛出的异常,或由协程创建的协程中抛出的异常,不会被 try/catch捕获!

SupervisorJob章节中,我们提到了 CoroutineExceptionHandler。让我们深入了解一下吧!


CoroutineExceptionHandler

CoroutineExceptionHandlerCoroutineContext的一个可选元素,用来处理未被捕获的异常

下面是如何定义一个 CoroutineExceptionHandler,当一个异常被捕获,你可以获取发生异常所在的 CoroutineContext的信息,以及异常本身:

val handler = CoroutineExceptionHandler { context, exception ->
    println("Caught $exception")
}

如果 CoroutineExceptionHandler满足以下条件,异常就会被捕获:

  • ⏰:异常是被协程自动抛出的(handler配合 launch使用,而不是 async
  • 🌍:如果 handler存在于在 CoroutineScope或者根协程( CoroutineScopesupervisorScope的直接子协程)的 CoroutineContext

让我们使用上面定义的 CoroutineExceptionHandler来看一些例子。在下面这个例子中,异常会被 handler捕获:

val scope = CoroutineScope(Job())
scope.launch(handler) {
    launch {
        throw Exception("Failed coroutine")
    }
}

下面这个例子,handler初始化于内部协程,异常不会被捕获:

val scope = CoroutineScope(Job())
scope.launch {
    launch(handler) {
        throw Exception("Failed coroutine")
    }
}

这个异常不会被捕获是因为 handler没有初始化于正确的 CoroutineContext。在内部的 launch中,异常一旦发生就会立马向上传播到父节点,由于父节点并不知道 handler,所以就会被抛出。


应用中合理的异常处理对于良好的用户体验非常重要,即使事情没有按照预期发展。

当异常发生的时候,如果想避免取消的传播,记得使用 SupervisorJob;相反,则使用 Job

未捕获的异常将被传播,捕获它们以提供好的用户体验!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值