首页 > 其他分享 >Kotlin | 关于协程异常处理,你想知道的都在这里

Kotlin | 关于协程异常处理,你想知道的都在这里

时间:2022-10-07 18:37:02浏览次数:50  
标签:SupervisorJob 协程 val launch Kotlin scope 异常

引言

关于协程的异常处理,一直以来都不是一个简单问题。因为涉及到了很多方面,包括 异常的传递结构化并发下的异常处理 ,异常的传播方式 ,不同的Job 等,所以常常让很多(特别是刚使用协程的,也不乏老手)同学摸不着头脑。

常见有如下两种处理方式:

  • ​try catch​
  • ​CoroutineExceptionHandler​

但这两种方式(特别是第二种)到底该什么时候用,用在哪里,却是一个问题?

比如虽然知道 ​​CoroutineExceptionHandler​​​ ,但为什么增加了却还是崩溃?到底应该加在哪里? 尝试半天发现无解,最终又只能直接 ​​try catch​​​ ,粗暴并有效,最终遇到此类问题,直接下意识 ​​try​​ 住。

​try catch​​​ 虽然直接,一定程度上也帮我们规避了很多使用方面的问题,但同时也埋下了很多坑,也就是说,并不是所有协程的异常都可以 ​​try​​ 住(取决于使用位置),其也不是任何场景的最优解。

鉴于此,本篇将从头到尾,帮助你理清以下问题:

  • 什么是结构化并发?
  • 协程的异常传播流程与形式
  • 协程的异常处理方式
  • 为什么有些异常处理了却还是崩了
  • SupervisorJob 的使用场景
  • ​supervisorScope​​​ 与​​coroutineScope​
  • 异常处理方式的场景推荐

本文尽可能会用大白话与你分享理解,如有遗漏或理解不当,也欢迎评论区反馈。

好了,让我们开始吧!


结构化并发

在最开始前,我们先搞清楚什么是 结构化并发,这对我们理解协程异常的传递将非常有帮助。

让我们先将思路转为日常业务开发中,比如在某某业务中,可能存在好几个需要同时处理的逻辑,比如同时请求两个网络接口,同时操作两个子任务等。我们暂且称上述学术化概念为 多个并发操作

而每个并发操作其实都是在处理一个单独的任务,这个 任务 中,可能还存在 子任务 ; 同样对于这个子任务来说,它又是其父任务的子单元。每个任务都有自己的生命周期,子任务的生命周期会继承父任务的生命周期,比如如果父任务关闭,子任务也会被取消。而如果满足这样特性,我们就称其就是 结构化并发

在协程中,我们常用的 ​​CoroutineScope​​,正是基于这样的特性,即其也有自己的作用域与层级概念。

比如当我们每次调用其扩展方法 ​​launch()​​​ 时,这个内部又是一个新的协程作用域,新的作用域又会与父协程保持着层级关系,当我们 取消 ​​CoroutineScope​​ 时,其所有子协程也都会被关闭。

如下代码片段:

val scope = CoroutineScope(Job())
val jobA = scope.launch(CoroutineName("A")) {
val jobChildA = launch(CoroutineName("child-A")) {
delay(1000)
println("xxx")
}
// jobChildA.cancel()
}
val jobB = scope.launch(CoroutineName("B")) {
delay(500)
println("xxx")
}
// scope.cancel()

我们定义了一个名为 ​​scope​​ 的作用域, 其中有两个子协程 jobA,B,同时 jobA 又有一个子协程 jobChildA。

如果我们要取消jobB,并不会影响jobA,其依然会继续执行;

但如果我们要取消整个作用域时 ​​scope.cancel()​​,jobA,jobB 都会被取消,相应 jobA 被取消时, 因为其也有自己的作用域,所以 jobChildA 也会被取消,以此类推。而这就是协程的 结构化并发特性


异常传播流程

默认情况下,任意一个协程发生异常时都会影响到整个协程树,而异常的传递通常是双向的,也即协程会向子协程与父协程共同传递,如下方所示:

Kotlin | 关于协程异常处理,你想知道的都在这里_Kotlin

整体流程如下:

  • 先 cancel 子协程
  • 取消自己
  • 将异常传递给父协程
  • (重复上述过程,直到根协程关闭)

举个例子,比如下面这段代码:

Kotlin | 关于协程异常处理,你想知道的都在这里_异常处理_02

在上图中,我们创建了 两个子协程A,B,并在 A中 抛出异常,查看结果如右图所示, 当子协程A异常被终止时,我们的子协程B与父协程都受到影响被终止。

当然如果不想在协程异常时,同级别子协程或者父协程受到影响,此时就可以使用 ​​SupervisorJob​​ ,这个我们放在下面再谈。


异常传播形式

在协程中,异常的传播形式有两种,一种是自动传播( ​​launch​​​ 或 ​​actor​​​),一种是向用户暴漏该异常( ​​async​​​ 或 ​​produce​​ ),这两种的区别在于,前者的异常传递过程是层层向上传递(如果异常没有被捕获),而后者将不会向上传递,会在调用处直接暴漏。

记住上述思路对我们处理协程的异常将会很有帮助。

异常处理方式

tryCatch

一般而言, ​​tryCath​​ 是我们最常见的处理异常方式,如下所示:

fun main() = runBlocking {
launch {
try {
throw NullPointerException()
} catch (e: Exception) {
e.printStackTrace()
}
}
println("嘿害哈")
}

当异常发生时,我们底部的输出依然能正常打印,这也不难理解,就像我们在 ​​Android​​​ 或者 ​​Java​​​ 中的使用一样。但有些时候这种方式并不一定能有效,我们在下面中会专门提到。但大多数情况下,​​tryCatch​​ 依然如万金油一般,稳定且可靠。


CoroutineExceptionHandler

其是用于在协程中全局捕获异常行为的最后一种机制,你可以理解为,类似 Thread.uncaughtExceptionHandler 一样。

但需要注意的是,​​CoroutineExceptionHandler​​​ 仅在未捕获的异常上调用,也即这个异常没有任何方式处理时(比如在源头tryCatch了),由于协程是结构化的,当子协程发生异常时,它会优先将异常委托给父协程区处理,以此类推 ​​直到根协程作用域或者顶级协程​​​ 。因此其永远不会使用我们子协程 ​​CoroutineContext​​​ 传递的 ​​CoroutineExceptionHandler​​​(​​SupervisorJob​​​ 除外),对于 ​​async​​ 这种,而是直接向用户直接暴漏该异常,所以我们在具体调用处直接处理就行。

如下示例所示:

val scope = CoroutineScope(Job())
scope.launch() {
launch(CoroutineExceptionHandler { _, _ -> }) {
delay(10)
throw

Kotlin | 关于协程异常处理,你想知道的都在这里_Android_03

不难发现异常了,原因就是我们的 ​​CoroutineExceptionHandler​​​ 位置不是根协程或者 ​​CoroutineScope​​ 初始化时。

如果我们改成下述方式,就可以正常处理该异常:

// 1. 初始化scope时
val scope = CoroutineScope(Job() + CoroutineExceptionHandler { _, _ -> })

// 2. 根协程

SupervisorJob

supervisorJob 是一个特殊的Job,其会改变异常的传递方式,当使用它时,我们子协程的失败不会影响到其他子协程与父协程,通俗点理解就是:子协程会自己处理异常,并不会影响其兄弟协程或者父协程,如下图所示:

Kotlin | 关于协程异常处理,你想知道的都在这里_结构化_04

举个简单的例子:

val scope = CoroutineScope(SupervisorJob() + CoroutineExceptionHandler { _, _ -> })
scope.launch(CoroutineName("A")) {
delay(10)
throw RuntimeException()
}
scope.launch(CoroutineName("B")) {
delay(100)
Log.e("petterp", "正常执行,我不会收到影响")
}

当协程A失败时,协程B依然可以正常打印。

如果我们将上述的示例改一下,会发生什么情况?如下所示:

val scope = CoroutineScope(CoroutineExceptionHandler { _, _ -> })
scope.launch(SupervisorJob()) {
launch(CoroutineName("A")) {
delay(10)
throw RuntimeException()
}
launch(CoroutineName("B")) {
delay(100)
Log.e("petterp", "正常执行,我不会收到影响")
}
}

猜一猜B协程内部的log能否正常打印?

结果是不能

为什么? 我不是已经使用了 SupervisorJob() 吗?我们用一张图来看一下:

Kotlin | 关于协程异常处理,你想知道的都在这里_Kotlin_05

如上图所示,我们在 ​​scope.launch​​​ 时传递了 ​​SupervisorJob​​​ ,看着似乎没什么问题

标签:SupervisorJob,协程,val,launch,Kotlin,scope,异常
From: https://blog.51cto.com/002test/5734833

相关文章

  • 捕获Java线程池执行任务抛出的异常
    捕获Java线程池执行任务抛出的异常Java中线程执行的任务接口java.lang.Runnable要求不抛出Checked异常,publicinterfaceRunnable{publicabstractvoidrun();......
  • 一个例子形象的理解协程和线程的区别
    一个例子形象的理解协程和线程的区别Talkischeap,showmethecode!所以,废话先不说,先上代码:首先写一个WebAPI接口///<summary>///测试接口///</summary>[RoutePrefix......
  • 一个例子形象的理解协程和线程的区别
    一个例子形象的理解协程和线程的区别Talkischeap,showmethecode!所以,废话先不说,先上代码:首先写一个WebAPI接口///<summary>///测试接口///</summary>[RoutePrefix......
  • 从 C# 崩溃异常 中研究页堆布局
    一:背景1.讲故事最近遇到一位朋友的程序崩溃,发现崩溃点在富编辑器msftedit上,这个不是重点,重点在于发现他已经开启了页堆,看样子是做了最后的挣扎。0:000>!analyze-......
  • Java异常处理的20个最佳实践
    titleshortTitlecategorytagdescriptionheadJava异常处理的20个最佳实践Java异常处理的20个最佳实践Java核心异常处理Java程序员进阶之路,......
  • 全局异常处理器
    @RestControllerAdvice//针对所有的RestController添加了AOPpublicclassBaseExceptionHandler{//指定捕获什么类型的异常@ExceptionHandler(value=Exce......
  • 线上Kafka突发rebalance异常,如何快速解决?
    Kafka是我们最常用的消息队列,它那几万、甚至几十万的处理速度让我们为之欣喜若狂。但是随着使用场景的增加,我们遇到的问题也越来越多,其中一个经常遇到的问题就是:rebalance......
  • SpringMVC之异常处理机制
    SpringBoot异常处理机制默认异常处理机制springboot默认提供了一个处理/error的handler,全局异常处理。对于机器客户端来说,产生JSON(具体的错误)、状态码和异常信息;对于......
  • 电网异常处理
    一、频率异常1、判定:正负0.2HZ,正负0.5HZ。2、异常处理:调整负荷,调整发电机出力,具体处理方法。二、电压异常处理1、判定:正负5%,正负10%。2、原因。3、电压低的处理。4......
  • 深入理解linux内核第三版(三)中断和异常
    中断:也叫异步中断,是由外设产生的。异常:也叫同步中断,是由CPU产生的,是指令执行过程中产生的。中断信号的作用:中断信号提供了一种特殊的方式,使处理器转而去运行正常控制流之......