• Kotlin 协程 - 协程异常处理器 CoroutineExceptionHandler


    一、异常的传播

    • Job的取消和异常传播是双向的(结构化并发),如果抛异常的代码在局部没有捕获处理,协程或将其传播到层次结构中,该协程会先取消所有子协程再取消自己,如果这个异常是 CancellationException 类型便终止向上传播,如果使用了 SupervisorScope() 或 supervisorJob 不管什么类型的异常都终止向上传播,否则会一直传递到根协程导致整个结构中的协程都会被取消。
    • CancellationException类型的异常由抛出的协程处理,SupervisorJob类型时由该层的下级子协程处理(可能是抛异常的协程或它的父协程,但是都是SupervisorJob层的子协程),其他情况由根协程处理。

    1.1 多个协程作用域之间的关系

    类型异常传播特征场景举例
    顶级作用域不向外传播。1.根协程之间。2.GlobalScope嵌套GlobalScope彼此独立互不影响。3.A和B是两个作用域对象,A开启的作用域中B开启了作用域,两个作用域彼此独立互不影响。4.supervisorScope() 或 supervisorJob 由于使用了新的 Job,相当于是一个独立的根协程,与外部互不影响。
    协同作用域双向传播。外层有父协程,且自身非另外的作用域对象开启。
    主从作用域向下单向传播。外层有父协程,自身是supervisorScope()或supervisorJob。与内部直接子协程主从,与外部协同。
    1. //【顶级作用域】1和2没有关系,1和3没有关系,2和3没有关系
    2. GlobalScope.launch { //协程1
    3. GlobalScope.launch{} //协程2
    4. }
    5. CoroutineScopr(Dispatcher.IO).lacunch{} //协程3
    6. //2和3是1的子协程,2或3异常都会取消1,1异常会取消2和3,2异常会取消3
    7. GlobalScope.launch { //协程1
    8. coroutineScoope {
    9. launch{} //协程2
    10. launch{} //协程3
    11. }
    12. }
    13. //4和5是3的子协程,4异常会取消5不会取消3,3异常会取消4和5不会取消2和1
    14. GlobalScope.launch { //协程1
    15. launch{} //协程2
    16. supervisorScope { //协程3
    17. launch{} //协程4
    18. launch{} //协程5
    19. }
    20. }

    二、打断传播

    当不希望异常向上传播或兄弟协程相互影响时使用(向下传播依然存在)。SupervisorScope() 使用的 supervisorJob,使用新的 Job 开启协程作用域意味着它是独立的,因此只需要关心自己内部,异常不处理会导致程序崩溃

    2.1 supervisorJob

    1. //避免在只有一个子协程的时候传参使用(孙协程仍会相互取消,作用相当于普通job)
    2. launch(SupervisorJob()) { //唯一子协程
    3. launch {
    4. println("子协程1")
    5. throw Exception() //会相互取消
    6. }
    7. launch {
    8. println("子协程2") //不会打印
    9. }
    10. }
    11. //正确写法
    12. launch {
    13. launch(SupervisorJob()) {
    14. println("子协程1")
    15. throw Exception() //不会相互取消
    16. }
    17. launch(SupervisorJob()) {
    18. println("子协程2") //会打印
    19. }
    20. }

    2.2 SupervisorScope()

    1. supervisorScope{
    2. launch{
    3. println("子协程1")
    4. throw Exception()
    5. }
    6. launch{
    7. println("子协程2") //会打印
    8. }
    9. }

    三、CancellationException

    如果异常是 CancellationException 及其子类,将不会向上传递,只取消当前协程及其子类。

    1. //分别开启1和2两个协程,抛出异常会结束1和它的子协程3,但不会影响2
    2. object MyException : CancellationException()
    3. suspend fun main(): Unit = coroutineScope {
    4. launch { //1
    5. launch { //3
    6. delay(2000)
    7. println("1") //不会打印
    8. }
    9. throw MyException
    10. }
    11. launch { //2
    12. delay(2000)
    13. println("2") //会打印
    14. }
    15. }

    四、异常捕获

    launch代码块中抛出的异常直接抛出。
    async代码块中抛出的异常通过最终消费即调用 await() 才抛出,子协程中的异常不受 await() 影响,未捕获会向上传递给根协程处理。所以对每个 await() 单独捕获是避免崩溃影响其它并发任务,再捕获全部 async 是避免子协程异常向上传递导致程序崩溃(也可以在外面套一层异常不向上传递的supervisorScope() 或 supervisorJob),或者使用CoroutineExceptionHandler。

    4.1 try-catch

    协程构建器捕获协程构建器无效,要捕获构建器代码块中具体抛异常的代码:如果协程代码块中 throw 的异常没有被捕获处理,就会被协程框架(即BaseContinuationImpl.resumeWith()中)捕获封装成 Result 对象传递,最终传递给异常处理器,不会再次throw,也就没有异常可捕获了,也就是构建器不抛异常。
    挂起函数能捕获到挂起函数中子线程的异常:try捕获子线程是无效的,只能捕获当前线程的堆栈信息。在协程中能捕获到开启了子线程的挂起函数中的异常,是因为挂起函数底层代码通过 reusmeWithExceptoon() 携带异常从子线程恢复到当前线程抛出,不然直接 throw 是捕获不到的还会导致永远挂起。
    1. //无效
    2. try {
    3. launch {
    4. //异常不被捕获不会再抛出,会在层次结构中双向传播实现结构化并发的连锁取消
    5. }
    6. }
    7. //有效
    8. launch {
    9. try { //具体会抛异常的代码 }
    10. }
    11. //有效
    12. try {
    13. coroutineScope { //具体会抛异常的代码 }
    14. }

    ​4.2 CoroutineExceptionHandler

    不会阻止异常传递,当执行时表示结构化并发已全部取消完成,是最后一次捕获异常。意思是无法从异常中恢复协程,只能用来做最后的处理(不捕获然就是线程的 UncaughtExceptionHandler 处理了),默认情况它会打印异常堆栈。

    层次结构中的上下文异常处理器设置在哪层生效
    全是Job时只有根协程设置了异常处理器才有效,父协程或自己设置了依旧崩。
    有supervisorJob或SupervisorScope()时从下往上,没遇到时设置了也没用,从遇到时起,不管设置在哪层或好几层都有设置,只有最近的那个生效。
    1. fun main(): Unit = runBlocking {
    2. val rootExceptionHandler = CoroutineExceptionHandler { _, throwable -> println("调用【根】协程异常处理器:${throwable.message}") }
    3. val parentExceptionHandler = CoroutineExceptionHandler { _, throwable -> println("调用【父】协程异常处理器:${throwable.message}") }
    4. val selfExceptionHandler = CoroutineExceptionHandler { _, throwable -> println("调用【自身】协程异常处理器:${throwable.message}") }
    5. val childExceptionHandler = CoroutineExceptionHandler { _, throwable -> println("调用【子】协程异常处理器:${throwable.message}") }
    6. //全是 Job 只使用 root,使用 parent 和 self 都无效会报错
    7. CoroutineScope(Job()).launch(rootExceptionHandler) {
    8. launch(parentExceptionHandler) {
    9. launch(selfExceptionHandler) {
    10. throw Exception("子协程使用的是Job")
    11. }
    12. }
    13. }
    14. //从下往上在遇到 SupervisorJob 或 supervisorScope() 起,使用最近的那个异常处理器
    15. //即child无效,有self用self,没有self用parent,没有parent用root,同时设置self、parent、root用最近的self
    16. CoroutineScope(Job()).launch(rootExceptionHandler) {
    17. launch(parentExceptionHandler) {
    18. launch(SupervisorJob() + selfExceptionHandler ) {
    19. launch(childExceptionHandler) {
    20. throw Exception("子协程使用的是SupervisorJob")
    21. }
    22. }
    23. }
    24. }
    25. delay(1000)
    26. }
  • 相关阅读:
    C语言动态实现顺序栈
    双向链表的知识点+例题
    在 MySQL 中模拟外部联接 (LEFT、RIGHT、INNER JOIN、OUTER JOIN)
    LC滤波器设计学习笔记(一)滤波电路入门
    公共Mono模块
    计算机毕业设计Java钢材出入库管理系统(源码+系统+mysql数据库+lw文档)
    DataX使用、同步MySQL数据到HDFS案例
    ES6基本语法(二)——函数与数组
    10.MySQL 约束
    Docker学习笔记
  • 原文地址:https://blog.csdn.net/HugMua/article/details/132797774