Kotlin 协程之取消与异常处理探索之旅(下)

阅读: 评论:0

Kotlin 协程之取消与异常处理探索之旅(下)

Kotlin 协程之取消与异常处理探索之旅(下)

前言

协程系列文章:

  • 一个小故事讲明白进程、线程、Kotlin 协程到底啥关系?
  • 少年,你可知 Kotlin 协程最初的样子?
  • 讲真,Kotlin 协程的挂起/恢复没那么神秘(故事篇)
  • 讲真,Kotlin 协程的挂起/恢复没那么神秘(原理篇)
  • Kotlin 协程调度切换线程是时候解开真相了
  • Kotlin 协程之线程池探索之旅(与Java线程池PK)
  • Kotlin 协程之取消与异常处理探索之旅(上)
  • Kotlin 协程之取消与异常处理探索之旅(下)
  • 来,跟我一起撸Kotlin runBlocking/launch/join/async/delay 原理&使用
  • 继续来,同我一起撸Kotlin Channel 深水区
  • Kotlin 协程 Select:看我如何多路复用
  • Kotlin Sequence 是时候派上用场了
  • Kotlin Flow 背压和线程切换竟然如此相似
  • Kotlin Flow啊,你将流向何方?
  • Kotlin SharedFlow&StateFlow 热流到底有多热?

上篇分析了线程异常&取消操作以及协程Job相关知识,有了这些基础知识,我们再来看协程的取消与异常处理就比较简单了。
通过本篇文章,你将了解到:

  1. 协程取消的几种方式
  2. 协程异常处理几种方式
  3. 协程异常传递原理

1. 协程取消的几种方式

非阻塞状态时取消

先看Demo:

class CancelDemo {fun testCancel() {runBlocking() {var job1 = launch(Dispatchers.IO) {println("job1 start")Thread.sleep(200)var count = 0while (count < 1000000000) {count++}println("job1 end count:$count")}Thread.sleep(100)println("start cancel job1")//取消job(取消协程)job1.cancel()println("end cancel job1")}}
}fun main(args: Array<String>) {var demo = CancelDemo()stCancel()Thread.sleep(1000000)
}

先启动一个子协程,它返回Job对象,当子协程成功运行后再取消它。
结果如下:

该打印反馈出两个信息:

  1. 子协程启动并运行后才开始取消它。
  2. 子协程并没有终止运行,而是正常运行到结束。

你可能对第2点比较困惑,为啥取消没效果呢?
还记得我们上篇分析的线程的终止吗?在非阻塞状态下,通过Thread.interrupt()调用下仅仅只是唤醒线程并且设置标记位。
与线程类似,协程Job.cancel()函数仅仅只是将state值改变而已,当然我们可以主动获取协程当前的状态。

        runBlocking() {var job1 = launch(Dispatchers.IO) {println("job1 start")Thread.sleep(80)var count = 0//判断协程的状态,若是活跃则继续循环//isActive = coroutineContext[Job]?.isActive ?: truewhile (count < 1000000000 && isActive) {count++}println("job1 end count:$count")}Thread.sleep(100)println("start cancel job1")//取消job(取消协程)job1.cancel()println("end cancel job1")}}

运行结果:

从打印结果可以看出:

协程确实被取消了,可以通过Job.isActive 判断取消是否成功,若Job.isActive = false 则表示协程被取消了。

阻塞状态时取消

说到阻塞状态,你可能会说:“简单,我几行代码就给你演示了:”

    fun testCancel3() {runBlocking() {var job1 = launch(Dispatchers.IO) {Thread.sleep(3000)println("coroutine isActive:$isActive")//①}Thread.sleep(100)println("start cancel job1")//取消job(取消协程)job1.cancel()println("end cancel job1")}}

先猜猜①会打印吗?有同学说不会打印,因为Thread.sleep(xx)方法会抛出异常。
实际结果却是:①会打印。
认为不会打印的同学可能将线程的阻塞与协程的阻塞(挂起)混淆了,Thread.sleep(xx)是阻塞协程所在的线程,它是线程的专属方法,因此它会响应线程的中断:Thread.interrupt()并抛出异常,而不会响应协程的Job.cancel()函数。
协程阻塞(挂起)并不会阻塞其所在的线程,改造Demo如下:

    fun testCancel4() {runBlocking() {var job1 = launch(Dispatchers.IO) {//协程挂起println("job1 start")delay(3000)println("coroutine isActive:$isActive")//①}Thread.sleep(100)println("start cancel job1")//取消job(取消协程)job1.cancel()println("end cancel job1")}}

观察打印结果,我们发现①始终无法打印出来,我们有理由相信协程执行到delay(xx)时抛出了异常,导致后续的代码无法执行,接着验证猜想。

    fun testCancel4() {runBlocking() {var job1 = launch(Dispatchers.IO) {//协程挂起println("job1 start")try {delay(3000)} catch (e : Exception) {println("delay exception:$e")}println("coroutine isActive:$isActive")//①}Thread.sleep(100)println("start cancel job1")//取消job(取消协程)job1.cancel()println("end cancel job1")}}

如上,给delay(xx)函数加了异常处理,打印结果如下:


果然不出所料,Job.cancel(xx)引发了delay(xx)异常,它抛出的异常为:JobCancellationException,该异常在JVM平台继承自CancellationException。

如何"优雅"地取消协程

结合阻塞/非阻塞状态下取消协程的分析,与线程处理方式类似:对于阻塞状态的协程,我们可以捕获异常,对于非阻塞的地方我们使用状态判断。
根据不同的结果来决定协程被取消后代码的处理逻辑。

    fun testCancel5() {runBlocking() {var job1 = launch(Dispatchers.IO) {try {//挂起函数} catch (e : Exception) {println("delay exception:$e")}if (!isActive) {println("cancel")}}}}

2. 协程异常处理几种方式

try…catch异常

上面提及了协程的取消异常,它是比较特殊的异常,我们先来看看普通的异常处理。

    fun testException() {runBlocking {try {var job1 = launch(Dispatchers.IO) {println("job1 start")//异常1 / 0println("job1 end")}} catch (e: Exception) {}}}

先猜猜这样能够捕获异常吗?根据我们上篇线程异常捕获的经验,此处的子协程运行在子线程里,在子线程里发生的异常,主线程当然无法通过try 捕获到。
当然,万能的方式是在子协程里捕获:

    fun testException2() {runBlocking {var job1 = launch(Dispatchers.IO) {try {println("job1 start")//异常1 / 0println("job1 end")} catch (e : Exception) {println("e=$e")}}}}

全局捕获异常

与线程类似,协程也可以全局捕获异常。

    //创建处理异常对象val exceptionHandler = CoroutineExceptionHandler { _, exception ->println("handle exception:$exception")}fun testException3() {runBlocking {//声明协程作用域var scope = CoroutineScope(Job() + exceptionHandler)var job1 = scope.launch(Dispatchers.IO) {println("job1 start")//异常1 / 0println("job1 end")}}}

如上Demo,先定义一个异常处理对象,然后将它与协程作用域关联起来。
当子协程发生了异常,这个异常往上抛给父Job,最后交给CoroutineExceptionHandler 处理。

此时,ArithmeticException 异常被CoroutineExceptionHandler 捕获了。
注,虽然能够捕获异常,但是发生异常的协程还是不能往下执行了。

3. 协程异常传递原理

协程对异常的再加工

launch{}

花括号里的内容即为协程体,而执行这部分的逻辑在sumeWith()函数里:

你可发现此处的重点?
这里将协程体的执行加了try…catch 捕获了,也就是说不论协程体里发生了什么异常,在这里都能够被捕获。
你可能会问了,既然能够捕获,为啥还会有异常抛出呢?我们有理由相信,协程内部一定记录了这个异常,然后在某个地方再次将它抛出。
此处捕获了异常之后,将它构造为Result,并记录在变量outcome里,接着看看后续对这个值的处理。
流程有点长,直接看调用栈:

重点看红色框里的两个函数。

#handleCoroutineExceptionImpl.kt
public fun handleCoroutineException(context: CoroutineContext, exception: Throwable) {try {//从context里取出异常处理对象,对应外部设置的全局捕获回调对象context[CoroutineExceptionHandler]?.let {//具体处理it.handleException(context, exception)//处理ok,直接退出return}} catch (t: Throwable) {handleCoroutineExceptionImpl(context, handlerException(exception, t))return}//再次尝试处理handleCoroutineExceptionImpl(context, exception)
}internal actual fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable) {// 尝试handler处理// 从当前线程抛出异常val currentThread = Thread.currentThread()currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception)
}

如果我们定义了CoroutineExceptionHandler,那么使用该Handler处理异常,如果没有定义,则直接抛出异常。
以上即为协程对异常的再加工处理过程。

异常在协程之间的传递(Job)

先看Demo:

    fun testException4() {runBlocking {//声明协程作用域var rootJob = Job()var scope = CoroutineScope(rootJob)var job1 = scope.launch(Dispatchers.IO + exceptionHandler) {println("job1 start")//异常1 / 0println("job1 end")}job1.join()//检查父Job 状态println("rootJob isActive:${rootJob.isActive}")}}

rootJob 作为父Job,通过launch(xx)函数创建了子Job:job1。
等待job1执行完毕后,再检查父Job 状态。
打印结果如下:

此时我们发现:

当子Job 发生异常时,会取消父Job。

除了对父Job 有影响,对其它兄弟Job 是否有影响呢?
继续做尝试:

    fun testException5() {runBlocking {//声明协程作用域var rootJob = Job()var scope = CoroutineScope(rootJob)var job1 = scope.launch(Dispatchers.IO + exceptionHandler) {println("job1 start")Thread.sleep(100)//异常1 / 0println("job1 end")}var job2 = scope.launch {println("job2 start")Thread.sleep(200)//检查jo2状态println("jo2 isActive:$isActive")}job1.join()//检查父Job 状态println("rootJob isActive:${rootJob.isActive}")}}

如上,父Job 分别创建了两个子Job:job1、job2,当job1 发生异常时,分别检测父Job与job2的状态,打印结果如下:

很明显得出结论:

当子Job 发生异常时,会将异常传递给父Job,父Job 先将自己名下的所有子Job都取消,然后将自己取消,最后继续将异常往上抛。

这部分的传递依靠Job 链完成,上篇文章我们有深入分析过Job 结构:

从源码分析其传递流程,先看调用栈:

重点看notifyCancelling(xx)函数:

#JobSupport.kt
//list == 子Job 链表
private fun notifyCancelling(list: NodeList, cause: Throwable) {//回调,忽略onCancelling(cause)//取消所有子JobnotifyHandlers<JobCancellingNode>(list, cause)//①//取消父JobcancelParent(cause) //②
}

分为两个要点:

#JobSupport.kt
private inline fun <reified T: JobNode> notifyHandlers(list: NodeList, cause: Throwable?) {var exception: Throwable? = nulllist.forEach<T> { node ->try {//遍历list,调用nodenode.invoke(cause)} catch (ex: Throwable) {//...}}//..
}

调用至此,实际上是ifyCancelling(xx),因为job1没有子Job,因此①处list 里没有节点。

#JobSupport.kt
private fun cancelParent(cause: Throwable): Boolean {val isCancellation = cause is CancellationExceptionval parent = parentHandleif (parent === null || parent === NonDisposableHandle) {//没有父Job,无法继续往上,停止return isCancellation}//取消父Jobreturn parent.childCancelled(cause) || isCancellation
}

如果你看过上篇文章的分析,再看此处就比较容易了,此处再贴一下Node 结构:

#JobSupport.kt
//主要有2个成员变量
//childJob: ChildJob 表示当前node指向的子Job
//parent: Job 表示当前node 指向的父Job
internal class ChildHandleNode(@JvmField val childJob: ChildJob
) : JobCancellingNode(), ChildHandle {override val parent: Job get() = job//父Job 取消其所有子Joboverride fun invoke(cause: Throwable?) = childJob.parentCancelled(job)//子Job向上传递,取消父Joboverride fun childCancelled(cause: Throwable): Boolean = job.childCancelled(cause)
}

对于①来说,list 里的node 为ChildHandleNode,node.invoke(cause)其实调用的就是childJob.parentCancelled(job),而childJob 表示每个子Job。

    #JobSupport.ktpublic final override fun parentCancelled(parentJob: ParentJob) {//遍历Job 下的子Job,取消它们cancelImpl(parentJob)}

就这么层层遍历下去,直至取消完所有层级的子Job。

而对于②而言,parent.childCancelled(cause)==job.childCancelled(cause),而job 表示的是当前job 的父Job。

    #JobSupport.ktpublic open fun childCancelled(cause: Throwable): Boolean {//如果是取消异常,则忽略if (cause is CancellationException) return true//取消父Jobreturn cancelImpl(cause) && handlesException}

这段代码透露出两个意思:

  1. 取消时候产生的异常称为"取消异常",该异常比较特殊,当某个job 发生异常时,它不会往上传递。
  2. 如果不是取消异常,则调用cancelImpl(xx)函数,该函数取消当前Job的所有子Job 与自己。

因为Job 链类似树的结构,因此异常传递是递归形式的。

Job 发生异常时,不仅取消自己名下的所有Job,也会取消父Job,往上递归直至根Job。

SupervisorJob 作用与原理

作用

子协程发生异常后,会取消父协程、兄弟协程的执行,这在有些场景是不合理的,因为伤害范围太广,明明是一个子协程的锅,非得所有协程来背。
还好官方考虑过这个问题,提供了SupervisorJob 来解决该问题。

    fun testException6() {runBlocking {//声明协程作用域var rootJob = SupervisorJob()var scope = CoroutineScope(rootJob)var job1 = scope.launch(Dispatchers.IO + exceptionHandler) {println("job1 start")Thread.sleep(100)//异常1 / 0println("job1 end")}var job2 = scope.launch {println("job2 start")Thread.sleep(200)//检查jo2状态println("jo2 isActive:$isActive")}job1.join()//检查父Job 状态println("rootJob isActive:${rootJob.isActive}")}}

仅仅改动了一个地方:将Job()换为SupervisorJob()。
结果如下:

job1 发生异常的时候,job2 和父job都没受到影响。

原理
当需要取消父Job 时,势必会调用到:job.childCancelled(cause)
而SupervisorJob 重写了该函数:

#Supervisor.kt
private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {override fun childCancelled(cause: Throwable): Boolean = false
}

不做任何处理,当然就不能取消父Job了,不能取消父Job,也就不能取消父Job 下的子Job。

对比Job()与SupervisorJob() 可知:

取消异常的传递

job.childCancelled(cause) 表示要取消父Job,而该函数实现里有对取消异常进行了特殊处理,因此取消异常不会往上传递。

    fun testException7() {runBlocking {//声明协程作用域var rootJob = SupervisorJob()var scope = CoroutineScope(rootJob)var job1 = scope.launch(Dispatchers.IO + exceptionHandler) {println("job1 start")Thread.sleep(2000)println("job1 end")}var job2 = scope.launch {println("job2 start")Thread.sleep(1000)//检查jo2状态println("jo2 isActive:$isActive")}Thread.sleep(300)job1.cancel()//检查父Job 状态println("rootJob isActive:${rootJob.isActive}")}}

取消job1,不会影响父Job,也不会影响子Job。

当取消父Job时,查看子Job 是否受影响。

    fun testException8() {runBlocking {//声明协程作用域var rootJob = SupervisorJob()var scope = CoroutineScope(rootJob)var job1 = scope.launch(Dispatchers.IO + exceptionHandler) {println("job1 start")Thread.sleep(2000)println("jo1 isActive:$isActive")}var job2 = scope.launch {println("job2 start")Thread.sleep(1000)//检查jo2状态println("jo2 isActive:$isActive")}Thread.sleep(300)rootJob.cancel()//检查父Job 状态println("rootJob isActive:${rootJob.isActive}")}}

当父Job 取消时,子Job 都会被取消。

至此,所有内容分析完毕,小结一下之前的内容:

  1. 协程的异常会沿着Job链传递,子协程发生异常会导致父协程(祖父协程…)、兄弟协程的取消。
  2. 若要防止上述情况,需要使用SupervisorJob作为父Job,它将忽略子Job产生的异常,不将它传递出去。
  3. 取消异常不会向上传递,父协程的取消会导致其下所有的子协程被取消。

关于协程的取消与异常处理到此分析完毕,下篇将分析launch/async/delay/runBlocking 的使用、原理以及异同点。

本文基于Kotlin 1.5.3,文中完整Demo请点击

您若喜欢,请点赞、关注、收藏,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android/Kotlin

1、Android各种Context的前世今生
2、Android DecorView 必知必会
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分发全套服务
6、Android invalidate/postInvalidate/requestLayout 彻底厘清
7、Android Window 如何确定大小/onMeasure()多次执行原因
8、Android事件驱动Handler-Message-Looper解析
9、Android 键盘一招搞定
10、Android 各种坐标彻底明了
11、Android Activity/Window/View 的background
12、Android Activity创建到View的显示过
13、Android IPC 系列
14、Android 存储系列
15、Java 并发系列不再疑惑
16、Java 线程池系列
17、Android Jetpack 前置基础系列
18、Android Jetpack 易学易懂系列
19、Kotlin 轻松入门系列
20、Kotlin 协程系列全面解读

本文发布于:2024-02-02 05:15:03,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170682210241590.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:之旅   异常   Kotlin
留言与评论(共有 0 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23