一、异常的传播
- 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)
}