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

本文详细解释了Kotlin协程中的异常传播规则,包括异常的双向传播、局部处理try-catch、不向上传播的CancelledException、supervisorJob和SupervisorScope的使用,以及根协程的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-catch 处理异常,异常就不会传播到父协程进而取消另一个,这会浪费资源,应该使用 CoroutineExceptionHandler 记录。如果两个协程之间没有依赖关系,需要异常不向上传播的话,使用 supervisorJob 或 SupervisorScope()。
挂起函数

        try捕获子线程是无效的,只能捕获当前线程的堆栈信息。在协程中能捕获到开启了子线程的挂起函数中的异常,是因为挂起函数底层代码通过 reusmeWithExceptoon() 携带异常从子线程恢复到当前线程抛出,不然在子线程直接 throw 当前线程是捕获不到的还会导致永远挂起。

        协程的取消是协作的,挂起函数(suspendCancelableCoroutine类型)作为检查点,在执行时会检查协程的取消,并在取消时抛出 CancellationException 来实现停掉后面代码的执行,这种异常将不会向上传递,只取消当前协程及其子协程,job.cancel() 就是这样取消某个子协程而不影响兄弟协程和父协程的。因此对挂起函数进行 try-catch 会导致 Job 取消失败引发内存泄漏或逻辑错误,必须在 catch 块中对 CancellationException 类型进行资源释放并再次抛出!

2.1 协程构建器

在 launch() 和 async() 协程中的未捕获异常都会立即向上传播到层次结构中。如果顶级协程是用 launch() 启动的,异常会被 CoroutineExceptionHandler 处理,或者传递给线程的未捕获异常处理程序。如果顶级协程是用 async() 启动的,异常会被封装在 Deferred 返回类型中,并在对其调用 .await() 时重新抛出。

launch自动传播。代码块中业务代码发生的异常是直接抛出,如果未 try-catch 处理就会被协程框架所捕获(不会再次抛出),然后在结构化并发中双向传播到根协程。
async

1.作为顶级协程:在 await() 调用处抛出。代码块中发生的异常会被封装在返回的 Deferred 中,并在调用 await() 最终消费时才抛出。如果对 await() 未 try-catch 处理就会被协程框架所捕获(不会再次抛出),然后再结构化并发中双向传播到根协程。所以对每个 await() 单独捕获是避免崩溃影响其它并发任务,捕获全部 await() 是避免子协程异常向上传递导致程序崩溃(也可以在外面套一层异常不向上传递的supervisorScope() 或 supervisorJob),或者使用CoroutineExceptionHandler。

2.作为子协程:直接抛出。异常会立即向上传播到层次结构中,即便没有调用 await()。

//无效
try {
    launch {
        //异常不被捕获不会再抛出,会在层次结构中双向传播实现结构化并发的连锁取消
    }
}

//有效
launch {
    try { //具体会抛异常的代码 }
}
val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, exception ->
    println("异常处理器:$exception")
}

val topLevelScope = CoroutineScope( coroutineExceptionHandler)
topLevelScope.launch {
    async {
        //此处 async 是子协程,异常会直接抛出
        throw RuntimeException("async发生异常")
    }
}
Thread.sleep(100)

2.2 挂起函数

runCatching {
    //捕获挂起函数,就需要过滤并抛出 CancellationException
}.onFailure {
    if (it is CancellationException) { throw it }
}

try {
    //捕获挂起函数,就需要过滤并抛出 CancellationException
} catch (e: Exception) {
    if (e is CancellationException) { throw e }
}

suspend fun demo() = coroutineScope() {
    try {
        //捕获业务代码,不影响挂起函数检查取消抛出 CancellationException
    } catch (e: Exception) {}
}
//分别开启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() 用于父协程。

        SupervisorScope() 的上下文使用了 supervisorJob,使用自定义 Job 而不是基于父协程构建,意味着失去结构化并发(父协程不会等待它完成,异常不会相互影响)。它是一个独立的作用域,是根协程,在其中启动的子协程是顶级协程,它必须自己处理异常而不会像 coroutineScope() 那样抛出。

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")    //会打印
    }
}
topLevelScope.launch {
    supervisorScope {
        //独立作用域中开启的协程是顶级协程,可以使用协程异常处理器
        val job = launch(coroutineExceptionHandler) {...}
    }
}

五、使用 CoroutineExceptionHandler

仅在未捕获的异常上生效(对已经 try-catch 处理过的不会生效),不会阻止异常传播(只能设置在launch根协程或作用域对象上,对async根协程设置无效需要捕获await()处理异常),当执行时表示结构化并发已全部取消完成,是最后一次捕获异常的机制。意思是无法从异常中恢复协程,只能用来做最后的处理(如记录异常、显示某种错误消息、终止或重启程序)(还不处理就是线程的 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)
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值