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

一、异常的传播

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

二、打断传播

当不希望异常向上传播或影响兄弟协程时使用(向下传播依然存在)。

2.1 supervisorJob

内部发生异常不cancel父协程及兄弟协程,也不会受他们影响,但内部的子协程发生异常会相互影响。异常不处理会导致程序崩溃。

Kotlin 复制代码
//避免在只有一个子协程的时候传参使用(孙协程仍会相互取消,作用相当于普通job)
launch(SupervisorJob()) {    //唯一子协程
    launch {
        println("子协程1")
        throw Exception()    //会相互取消
    }   
    launch {
        println("子协程2")    //不会打印
    }
}
//正确写法
val job = SupervisorJob()
launch(job) {
    println("子协程1")
    throw Exception()    //不会相互取消
}
launch(job) {
    println("子协程2")    //会打印
}

2.2 SupervisorScope()

Kotlin 复制代码
supervisorScope{
    launch{
        println("子协程1")
        throw Exception()
    }
    launch{
        println("子协程2")    //会打印
    }
}

三、CancellationException

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

Kotlin 复制代码
//分别开启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")    //会打印
    }
}

四、异常捕获

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

4.1 try-catch

  • 捕获协程构建器无效,要捕获协程代码块中 throw 的异常:如果协程代码块中 throw 的异常没有被捕获处理,就会在 BaseContinuationImpl.resumeWith() 中被捕获封装成 Result 对象传递,最终传递给异常处理器,不会再次throw,也就没有异常可捕获了,也就是构建器不抛异常。

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

4.2 CoroutineExceptionHandler

  • 异常会一直向上传播到根协程,根协程如果不作响应(SupervisorScope()或 supervisorJob),则直接子协程会在上下文中寻找 CoroutineExceptionHandler 处理,否则走 UncaughtExceptionHandler,所以其它子协程中的 CoroutineExceptionHandler 不会起作用。
  • 使用条件:要么存在于协程作用域的上下文中、要么存在于根协程的直接子协程中。
  • 只能处理当前作用域中的异常,例如代码块中调用了其它作用域对象开启的协程,发生的异常需要在该作用域对象的上下文中配置CoroutineExceptionHandler处理。
Kotlin 复制代码
//不会阻止异常传递,但可以定义在发生异常时的处理行为(默认情况它会打印异常堆栈)
fun main(): Unit = runBlocking {
    val parentExceptionHandler = CoroutineExceptionHandler { _, throwable -> println("调用根协程异常处理器:${throwable.message}") }
    val childExceptionHandler = CoroutineExceptionHandler { _, throwable -> println("调用子协程异常处理器:${throwable.message}") }
    CoroutineScope().launch(parentExceptionHandler) {
        launch(childExceptionHandler) {
            throw Exception("子协程使用的是Job")    //打印:调用根协程异常处理器
        }
    }
    CoroutineScope().launch(parentExceptionHandler) {
        launch(SupervisorJob() + childExceptionHandler) {
            throw Exception("子协程使用的是SupervisorJob")    //打印:调用子协程异常处理器
        }
    }
    delay(1000)
}
相关推荐
xvch3 小时前
Kotlin 2.1.0 入门教程(八)
android·kotlin
zhangphil15 小时前
Android BitmapShader简洁实现马赛克,Kotlin(二)
android·kotlin
柯南二号16 小时前
Kotlin泛型学习篇
开发语言·python·kotlin
xvch1 天前
Kotlin 2.1.0 入门教程(九)
android·kotlin
闲暇部落1 天前
多线程详解——Kotlin多线程几种实现方式
android·kotlin·多线程
zhangphil2 天前
Android ValueAnimator ImageView animate() rotation,Kotlin
android·kotlin
命运之手2 天前
[ Spring ] Nacos Config Auto Refresh 2025
spring·nacos·kotlin·config·refresh
闲暇部落2 天前
kotlin内联函数——let,run,apply,also,with的区别
kotlin·内联函数
五味香2 天前
Java学习,List 元素替换
android·java·开发语言·python·学习·golang·kotlin
xidianjiapei0012 天前
为何应将微服务从Java迁移到Kotlin:经验与见解【来自DZone】
java·微服务·kotlin