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

一、概念

1.1 异常的传播

异常在局部未 try-catch 处理,会被协程框架捕获(不会再次抛出)经由结构化并发双向传播取消,该协程会先取消所有子协程再取消自己,如果异常是 CancellationException 类型便到此为止,如果是其它类型会继续向上传播,途中如果没有碰到 SupervisorJob 或 SupervisorScope() 会一直传播到根协程导致整个协程结构被取消,如果根协程没有设置 CoroutineExceptionHandler 就会交给线程处理,如果线程没有设置 UncaughtExceptionHandler,就会导致崩溃。

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

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

//2和3是1的子协程,2或3异常都会取消1,1异常会取消2和3,2异常会取消3
GlobalScope.launch {    //协程1
    coroutineScoope {
         launch{}    //协程2
         launch{}    //协程3
    }
}

//4和5是3的子协程,4异常会取消5不会取消3,3异常会取消4和5不会取消2和1
GlobalScope.launch {    //协程1
    launch{}    //协程2
    supervisorScope {    //协程3
         launch{}    //协程4
         launch{}    //协程5
    }
}

二、局部处理 try-catch

异常在局部未 try-catch 处理,会被协程框架捕获(不会再次抛出)经由结构化并发双向传播取消。

协程构建器捕获协程构建器无效,要捕获构建器代码块中具体抛异常的代码:如果代码块中业务代码发生的异常没有被 try-catch 处理,就会被协程框架(即BaseContinuationImpl.resumeWith()中)捕获封装成 Result 对象双向传递,不会再次抛出,也就没有异常可捕获了,也就是构建器不抛异常。
挂起函数能捕获到挂起函数中子线程的异常:try捕获子线程是无效的,只能捕获当前线程的堆栈信息。在协程中能捕获到开启了子线程的挂起函数中的异常,是因为挂起函数底层代码通过 reusmeWithExceptoon() 携带异常从子线程恢复到当前线程抛出,不然直接 throw 是捕获不到的还会导致永远挂起。
launch自动传播。代码块中业务代码发生的异常是直接抛出,如果未 try-catch 处理就会被协程框架所捕获(不会再次抛出),然后在结构化并发中双向传播到根协程。
async在调用处暴露。代码块中业务代码发生的异常通过最终消费才抛出,即调用 await(),如果未 try-catch 处理就会被协程框架所捕获(不会再次抛出),然后再结构化并发中双向传播到根协程。所以对每个 await() 单独捕获是避免崩溃影响其它并发任务,再捕获全部 async 是避免子协程异常向上传递导致程序崩溃(也可以在外面套一层异常不向上传递的supervisorScope() 或 supervisorJob),或者使用CoroutineExceptionHandler。
//无效
try {
    launch {
        //异常不被捕获不会再抛出,会在层次结构中双向传播实现结构化并发的连锁取消
    }
}

//有效
launch {
    try { //具体会抛异常的代码 }
}

//有效
try {
    coroutineScope { //具体会抛异常的代码 }
}

三、不向上传播 CancellationException

如果异常是 CancellationException 及其子类,将不会向上传递,只取消当前协程及其子类。job.cancel() 就是这样取消某个子协程而不影响兄弟协程和父协程的。

//分别开启1和2两个协程,抛出异常会结束1和它的子协程3,但不会影响2
object MyException : CancellationException()
suspend fun main(): Unit = coroutineScope {
    launch {    //1
        launch {    //3
            delay(2000)
            println("1")    //不会打印(向下传播了)
        }
        throw MyException
    }
    launch {    //2
        delay(2000)
        println("2")    //会打印(没有向上传播)
    }
}

四、打断向上传播 supervisorXXX

当不希望子协程发生的异常向上传播取消父协程或取消兄弟协程时使用(向下传播依然存在),即让子协程处理自己的异常。supervisorJob 用于同级子协程,SupervisorScope() 用于父协程。

4.1 supervisorJob

//错误用法:设置给了父协程,无法阻止向下传播
launch(SupervisorJob()) {
    val job1 = launch {
        println("子协程1")
        throw Exception()    //会取消子协程2
    }   
    val job2 = launch {
        println("子协程2")    //不会打印
    }
}

//正确用法:在同级子协程之间使用
launch {
    launch(SupervisorJob()) {
        println("子协程1")
        throw Exception()    //不会取消子协程2
    }
    launch(SupervisorJob()) {
        println("子协程2")    //会打印
    }
}

4.2 SupervisorScope()

supervisorScope{
    launch{
        println("子协程1")
        throw Exception()
    }
    launch{
        println("子协程2")    //会打印
    }
}

五、最后根协程处理 CoroutineExceptionHandler

仅在未捕获的异常上生效(对已经 try-catch 处理过的不会生效),不会阻止异常传播(只能设置在根协程或作用域对象上),当执行时表示结构化并发已全部取消完成,是最后一次捕获异常的机制。意思是无法从异常中恢复协程,只能用来做最后的处理(还不处理就是线程的 UncaughtExceptionHandler 处理了),默认情况它会打印异常堆栈。

层次结构中的上下文情况设置在哪层生效
全是Job时因为不会阻止异常传播,只有根协程或协程作用域对象设置了才有效,父协程或自己设置了依旧崩。
有supervisorJob或SupervisorScope()时从下往上,没遇到时设置了也没用,从遇到时起,不管设置在哪层或好几层都有设置,只有最近的那个生效。
fun main(): Unit = runBlocking {
    val rootExceptionHandler = CoroutineExceptionHandler { _, throwable -> println("调用【根】协程异常处理器:${throwable.message}") }
    val parentExceptionHandler = CoroutineExceptionHandler { _, throwable -> println("调用【父】协程异常处理器:${throwable.message}") }
    val selfExceptionHandler = CoroutineExceptionHandler { _, throwable -> println("调用【自身】协程异常处理器:${throwable.message}") }
    val childExceptionHandler = CoroutineExceptionHandler { _, throwable -> println("调用【子】协程异常处理器:${throwable.message}") }

    //全是 Job 只使用 root,使用 parent 和 self 都无效会报错
    CoroutineScope(Job()).launch(rootExceptionHandler) {
        launch(parentExceptionHandler) {
            launch(selfExceptionHandler) {
                throw Exception("子协程使用的是Job")
            }
        }
    }

    //从下往上在遇到 SupervisorJob 或 supervisorScope() 起,使用最近的那个异常处理器
    //即child无效,有self用self,没有self用parent,没有parent用root,同时设置self、parent、root用最近的self
    CoroutineScope(Job()).launch(rootExceptionHandler) {
        launch(parentExceptionHandler) {
            launch(SupervisorJob() + selfExceptionHandler ) {
                launch(childExceptionHandler) {
                    throw Exception("子协程使用的是SupervisorJob")
                }
            }
        }
    }
    delay(1000)
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值