[译] 如何优雅的处理协程的异常?

简介: [译] 如何优雅的处理协程的异常?

本文是 协程的取消和异常 系列的第三篇,往期目录如下:

Coroutines: First things first

如何优雅的处理协程的取消?

在阅读本文之前,强烈建议回顾一下之前两篇文章。实在没有时间的话,至少读一下第一篇文章。


下面开始正文。

作为开发者,我们通常会花费大量时间来完善我们的应用。但是,当发生异常导致应用不按预期执行时尽可能的提供良好的用户体验也是同样重要的。一方面,应用 Crash 对用户来说是很糟糕的体验;另一方面,当用户操作失败时,提供正确的信息也是必不可少的。


优雅的异常处理对用户来说是很重要的。在这篇文章中,我会介绍在协程中异常是怎么传播的,以及如何使用各种方式控制异常的传播。

如果你更喜欢视频,可以观看 Florina Muntenescu 和我 在 KotlinConf'19 上的演讲,地址如下:

https://www.youtube.com/watch?v=w0kfnydnFWI&feature=emb_logo

为了帮你更好的理解本文的剩余内容,建议首先阅读该系列的第一篇文章 Coroutines: First things first


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


当一个协程发生了异常,它将把异常传播给它的父协程,父协程会做以下几件事:

  1. 取消其他子协程
  2. 取消自己
  3. 将异常传播给自己的父协程

异常最终将传播至继承结构的根部。通过该 CoroutineScope 创建的所有协程都将被取消。

image.png

在某些场景下,这样的异常传播是适用的。但是,也有一些场景并不合适。

想象一个 UI 相关的 CoroutineScope ,它负责处理用户交互。如果它的一个子协程抛出了异常,那么这个 UI Scope 将被取消。由于被取消的作用域无法启动更多协程,整个 UI 组件将无法响应用户交互。


如果你不想要这样怎么办?或许,在创建协程作用域的 CoroutineContext 时,你可以选择不一样的 Job 实现 —— SupervisorJob


让 SupervisorJob 拯救你


通过 SupervisorJob,子协程的失败不会影响其他的子协程。此外,SupervisorJob 也不会传播异常,而是让子协程自己处理。


你可以这样创建协程作用域 val uiScope = CoroutineScope(SupervisorJob()) ,来保证不传播异常。

image.png

如果异常没有被处理,CoroutineContext 也没有提供异常处理器 CoroutineExceptionHandler (稍后会介绍),将会使用默认的异常处理器。在 JVM 上,异常会被打印到控制台;在 Android 上,无论发生在什么调度器上,你的应用都会崩溃。


💥 无论你使用哪种类型的 Job,未捕获异常最终都会被抛出。

同样的行为准则也适用于协程作用域构建器 coroutineScopesupervisorScope 。它们都会创建一个子作用域(以 Job 或者 SupervisorJob 作为 Parent),来帮助你给协程从逻辑上分组(如果你想进行并行计算,或者它们是否会相互影响)。

警告:SupervisorJob 仅在属于下面两种作用域时才起作用:使用 supervisorScope 或者 CoroutineScope(SupervisorJob()) 创建的作用域。


Job 还是 SupervisorJob ?🤔

什么时候使用 Job ?什么时候使用 SupervisorJob

当你不想让异常导致父协程和兄弟协程被取消时,使用 SupervisorJob 或者 supervisorScope


看看下面这个示例:

// Scope handling coroutines for a particular layer of my app
val scope = CoroutineScope(SupervisorJob())
scope.launch {
    // Child 1
}
scope.launch {
    // Child 2
}
复制代码


在这样的情况下,child#1 失败了,scopechild#2 都不会被取消。

另一个示例:

// Scope handling coroutines for a particular layer of my app
val scope = CoroutineScope(Job())
scope.launch {
    supervisorScope {
        launch {
            // Child 1
        }
        launch {
            // Child 2
        }
    }
}
复制代码


在这种情况下,supervisorScope 创建了一个携带 SupervisorJob 的子作用域。如果 child#1 失败,child#2 也不会被取消。但是如果使用 coroutineScope 来代替 supervisorScope 的话,异常将会传播并取消作用域。


测试!谁是我的父亲 ?🎯

通过下面的代码段,你能确定 child#1 的父级是哪一种 Job 吗?

val scope = CoroutineScope(Job())
scope.launch(SupervisorJob()) {
    // new coroutine -> can suspend
   launch {
        // Child 1
    }
    launch {
        // Child 2
    }
}
复制代码


child#1 的父 Job 是 Job 类型 !希望你回答正确!尽管第一眼看上去,你可能认为是 SupervisorJob,但并不是。因为在这种情况下,每个新的协程总是被分配一个新的 Job,这个新的 Job 覆盖了 SupervisorJobSupervisorJob 是父协程通过 scope.launch 创建的。也就是说,在上面的例子中,SupervisorJob 没有发挥任何作用。

image.png

The parent of child#1 and child#2 is of type Job, not SupervisorJob

所以,无论是 child#1 还是 child#2 发生了异常,都将传播到 scope,并导致所有由其启动的协程被取消。


记住 SupervisorJob 仅在属于下面两种作用域时才起作用:使用 supervisorScope 或者 CoroutineScope(SupervisorJob()) 创建的作用域。SupervisorJob 作为参数传递给协程构建器并不会产生你所预期的效果。


关于异常,如果子协程抛出了异常,SupervisorJob 不会进行传播并让子协程自己去处理。


原理

如果你好奇 Job 的工作原理,可以在 JobSupport.kt 文件中查看 childCancellednotifyCancelling 这两个函数的实现。

对于 SupervisorJob 的实现,childCancelled() 方法仅仅只是返回 false ,表示它不会传播异常,同时也不会处理异常。


异常的处理 👩‍🚒


在协程中,可以使用常规语法来处理异常:try/catch 或者内置的函数 runCatching (内部使用了 try/catch) 。

我们之前说过 未捕获的异常始终会被抛出 。但是不同的协程构建器对于异常有不同的处理方式。


Launch

在 launch 中,异常一旦发生就会立马被抛出 。因此,你可以使用 try/catch 包裹会发生异常的代码。如下所示:

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

在 launch 中,异常一旦发生就会立马被抛出 。


Async

async 在根协程 (CoroutineScope 实例或者 supervisorJob 的直接子协程) 使用时,异常不会被自动抛出,而是直到你调用 .await() 时才抛出。

为了处理 async 抛出的异常,你可以在 try/catch 中调用 await

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


在上面的例子中,async 的调用处永远不会抛出异常,所以这里并不需要包裹 try/catchawait() 方法将会抛出 async 内部发生的异常。

注意上面的代码中我们使用的是 supervisorScope 来调用 asyncawait 。就像之前说过的那样,SupervisorJob 让协程自己处理异常。与之相反的,Job 会传播异常,所以 catch 代码块不会被调用。


coroutineScope {
    try {
        val deferred = async {
            codeThatCanThrowExceptions()
        }
        deferred.await()
    } catch(e: Exception) {
        // Exception thrown in async WILL NOT be caught here 
        // but propagated up to the scope
    }
}
复制代码


此外,由其他协程创建的协程如果发生了异常,也将会自动传播,无论你的协程构建器是什么。

举个例子:

val scope = CoroutineScope(Job())
scope.launch {
    async {
        // If async throws, launch throws without calling .await()
    }
}
复制代码


在上面的例子中,如果 async 发生了异常,会立即被抛出。因为 scope 的直接子协程是由 scope.launch 启动的,async 继承了协程上下文中的 Job ,导致它会自动向父级传播异常。

⚠️ 通过 coroutineScope 构建器或者由其他协程启动的协程抛出的异常,不会被 try/catch 捕获!

SupervisorJob 那一节,我们提到了 CoroutineExceptionHandler 。现在让我们来深入了解它。


CoroutineExceptionHandler

协程异常处理器 CoroutineExceptionHandler 是 CoroutineContext 中的一个可选元素,它可以帮助你 处理未捕获异常


下面的代码展示了如何定义一个 CoroutineExceptionHandler 。无论异常何时被捕获,你都会得到关于发生异常的 CoroutineContext 的信息,和异常本身的信息。


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


如果满足以下要求,异常将会被捕获:

  • 何时⏰ :是被可以自动抛异常的协程抛出的(launch,而不是 async
  • 何地🌍 :在 CoroutineScope 或者根协程的协程上下文中(CoroutineScope 的直接子协程或者 supervisorScope

让我们看两个 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 没有在正确的协程上下文中使用,所以异常没有被捕获。内部 launch 启动的协程一旦发生异常会自动传播到父协程,而父协程并不知道 handler 的存在,所以异常会被直接抛出。


即使你的应用因为异常没有按照预期执行,优雅的异常处理对于良好的用户体验也是很重要的。

当你要避免因异常自动传播造成的协程取消时,记住使用 SupervisorJob ,否则请使用 Job

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


这篇文章就到这里了,这个系列还剩最后一篇了。

在之前提到协程的取消时,介绍了 viewModelScope 等跟随生命周期自动取消的协程作用域。但是不想取消时,应该怎么做?下一篇将会为你解答。



相关文章
|
NoSQL 数据可视化 JavaScript
漂亮又好用的Redis可视化客户端汇总
漂亮又好用的Redis可视化客户端汇总
10645 1
漂亮又好用的Redis可视化客户端汇总
|
机器学习/深度学习 人工智能 达摩院
[ICML'22] 阿里巴巴达摩院FEDformer,长程时序预测全面超越SOTA
本文介绍阿里巴巴达摩院决策智能实验室时间序列预测方向的最新(ICML 2022 accepted)工作:FEDformer: Frequency Enhanced Decomposed Transformer for Long-term Series Forecasting论文链接:https://arxiv.org/abs/2201.12740代码链接:https://github.com/DA
3916 0
[ICML'22] 阿里巴巴达摩院FEDformer,长程时序预测全面超越SOTA
|
4月前
|
人工智能 数据可视化 机器人
OpenClaw一键部署攻略,手把手教你 “养龙虾”!
还在为部署OpenClaw踩坑发愁?“养龙虾”其实超简单!本文奉上阿里云一键云端部署攻略:全程可视化、零代码,仅两步——买预装服务器+填API密钥,5分钟即可拥有专属AI数字员工!支持微信/钉钉协同、文件处理、日程管理、代码辅助等,新手友好,成本低廉(新用户首月9.9元+7000万Token免费额度)。
797 25
|
定位技术 API
HarmonyOS实战:高德地图定位功能完整流程详解
本文详细介绍了在鸿蒙系统中使用高德地图实现完整定位功能的流程。首先分析需求,包括权限申请、检查GPS状态、单次或多次定位选择以及定位失败处理。接着通过代码实现具体步骤:添加定位权限、申请用户权限、检查GPS开关状态、启动定位服务,并处理定位成功或失败的情况。若定位失败,可尝试获取历史定位信息或使用默认位置。最后总结指出,虽然定位功能基础简单,但完整的流程与细节处理才是关键。建议读者动手实践,掌握高德地图定位功能的使用。
1559 15
|
12月前
|
缓存 JSON 数据库
检验你的fastapi掌握了吗
本内容系统讲解了 FastAPI 的核心功能与高级应用,包括路径参数定义、类型验证、Pydantic 模型、依赖注入、异步处理、权限校验、CORS 配置、错误处理、文档生成及性能优化等内容,适用于构建高效、可维护的现代 Web API 服务。
540 7
|
监控 Ubuntu Linux
Windows11 WSL2 Ubuntu编译安装perf工具
通过以上步骤,你已经在Windows 11的WSL2中成功编译并安装了 `perf`工具。尽管在WSL2中可能会遇到一些限制,但大部分基本性能分析功能应当可以正常使用。使用 `perf`进行性能分析,可以帮助你更好地理解和优化系统及应用程序的性能。
1186 14
|
人工智能 自然语言处理 搜索推荐
阿里云Elasticsearch AI搜索实践
本文介绍了阿里云 Elasticsearch 在AI 搜索方面的技术实践与探索。
20566 22
|
机器学习/深度学习 并行计算 编译器
AVX 指令集简介及其与 SSE 的对比
本文介绍了AVX指令集在高性能计算中的重要性,它是Intel于2011年推出的一种SIMD技术,扩展了SSE指令集,将向量宽度增至256位,支持更多数据类型和浮点精度控制。主要差异包括向量宽度、数据类型扩展、指令集增加和精度控制。文中通过C代码示例展示了如何使用AVX进行向量加法。AVX对科学计算、图像处理和机器学习等领域提供了显著的性能提升。编译时需确保编译器支持AVX标志。
|
存储 数据处理 C++
内存 vs 硬盘:固态硬盘代替内存可以工作吗?
内存 vs 硬盘:固态硬盘代替内存可以工作吗?
1034 2