Kotlin 协程的异常处理准则
作者:xiangxiongfly915
Kotlin 协程的异常处理
概述
协程是互相协作的程序,协程是结构化的。
正是因为协程的这两个特点,导致它和 Java 的异常处理机制不一样。如果将 Java 的异常处理机制照搬到Kotlin协程中,会遇到很多问题,如:协程无法取消、try-catch不起作用等。
Kotlin协程中的异常主要分两大类
- 协程取消异常(CancellationException)
- 其他异常
异常处理六大准则
- 协程的取消需要内部配合。
- 不要打破协程的父子结构。
- 捕获 CancellationException 异常后,需要考虑是否重新抛出来。
- 不要用 try-catch 直接包裹 launch、async。
- 使用 SurpervisorJob 控制异常传播的范围。
- 使用 CoroutineExceptionHandler 处理复杂结构的协程异常,仅在顶层协程中起作用。
核心理念:协程是结构化的,异常传播也是结构化的。
准则一:协程的取消需要内部配合
协程任务被取消时,它的内部会产生一个 CancellationException 异常,协程的结构化并发的特点:如果取消了父协程,则子协程也会跟着取消。
问题:cancel不被响应
fun main() = runBlocking { val job = launch(Dispatchers.Default) { var i = 0 while (true) { Thread.sleep(500L) i++ println("i: $i") } } delay(200L) job.cancel() job.join() println("End") } /* 输出信息: i: 1 i: 2 i: 3 i: 4 // 不会停止,一直打印输出 */
原因:协程是相互协作的程序,因此协程任务的取消也需要相互协作。协程外部取消,协程内部需要做出相应。
解决:使用 isActive 判断是否处于活跃状态
使用 isActive 判断协程的活跃状态。
fun main() = runBlocking { val job = launch(Dispatchers.Default) { var i = 0 // 关键 // ↓ while (isActive) { Thread.sleep(500L) i++ println("i: $i") } } delay(200L) job.cancel() job.join() println("End") } /* 输出信息: i: 1 End */
准则二:不要打破协程的父子结构
问题:子协程不会跟随父协程一起取消
val fixedDispatcher = Executors.newFixedThreadPool(2) { Thread(it, "MyFixedThread") }.asCoroutineDispatcher() fun main() = runBlocking { // 父协程 val parentJob = launch(fixedDispatcher) { //子协程1 launch(Job()) { var i = 0 while (isActive) { Thread.sleep(500L) i++ println("子协程1 i:$i") } } //子协程2 launch { var i = 0 while (isActive) { Thread.sleep(500L) i++ println("子协程2 i:$i") } } } delay(1000L) parentJob.cancel() parentJob.join() println("End") } /* 输出信息: 子协程1 i:1 子协程2 i:1 子协程2 i:2 子协程1 i:2 End 子协程1 i:3 子协程1 i:4 子协程1 i:5 // 子协程1一直在执行,不会停下来 */
原因:协程是结构化的,取消啦父协程,子协程也会被取消。但是在这里“子协程1”不在 parentJob 的子协程,打破了原有的结构化关系,当调用 parentJob.cancel 时,“子协程1”就不会被取消了。
解决:不破坏父子结构
“子协程1”不要传入额外的 Job()。
fun main() = runBlocking { val parentJob = launch(fixedDispatcher) { launch { var i = 0 while (isActive) { Thread.sleep(500L) i++ println("子协程1:i= $i") } } launch { var i = 0 while (isActive) { Thread.sleep(500L) i++ println("子协程2:i= $i") } } } delay(2000L) parentJob.cancel() parentJob.join() println("end") } /* 输出结果: 子协程1:i= 1 子协程2:i= 1 子协程2:i= 2 子协程1:i= 2 子协程1:i= 3 子协程2:i= 3 子协程1:i= 4 子协程2:i= 4 end */
准则三:捕获 CancellationException 需要重新抛出来
挂起函数可以自动响应协程的取消
Kotlin 中的挂起函数是可以自动响应协程的取消,如下中的 delay() 函数可以自动检测当前协程是否被取消,如果已经取消了它就会抛出一个 CancellationException,从而终止当前协程。
fun main() = runBlocking { // 父协程 val parentJob = launch(Dispatchers.Default) { //子协程1 launch { var i = 0 while (true) { // 这里 delay(500L) i++ println("子协程1 i:$i") } } //子协程2 launch { var i = 0 while (true) { // 这里 delay(500L) i++ println("子协程2 i:$i") } } } delay(1000L) parentJob.cancel() parentJob.join() println("End") } /* 输出信息: 子协程1 i:1 子协程2 i:1 子协程1 i:2 子协程2 i:2 End */
fun main() = runBlocking { // 父协程 val parentJob = launch(Dispatchers.Default) { //子协程1 launch { var i = 0 while (true) { try { delay(500L) } catch (e: CancellationException) { println("捕获CancellationException") throw e } i++ println("子协程1 i:$i") } } //子协程2 launch { var i = 0 while (true) { try { delay(500L) } catch (e: CancellationException) { println("捕获CancellationException") throw e } i++ println("子协程2 i:$i") } } } delay(1000L) parentJob.cancel() parentJob.join() println("End") } /* 输出信息: 子协程1 i:1 子协程2 i:1 捕获CancellationException 捕获CancellationException End */
问题:捕获 CancellationException 导致崩溃
fun main() = runBlocking { val parentJob = launch(Dispatchers.Default) { launch { var i = 0 while (true) { try { delay(500L) } catch (e: CancellationException) { println("捕获CancellationException异常") } i++ println("子协程1 i= $i") } } launch { var i = 0 while (true) { delay(500L) i++ println("子协程2 i= $i") } } } delay(2000L) parentJob.cancel() parentJob.join() println("end") } /* 输出信息: 子协程1 i= 1 子协程2 i= 1 子协程1 i= 2 子协程2 i= 2 子协程1 i= 3 子协程2 i= 3 捕获CancellationException异常 ...... //程序不会终止 */
原因:当捕获到 CancellationException 以后,还需要将它重新抛出去,如果没有抛出去则子协程将无法取消。
解决:需要重新抛出
重新抛出异常,执行 throw e
。
以上三条准则,都是应对 CancellationException 这个特殊异常的。
fun main() = runBlocking { val parentJob = launch(Dispatchers.Default) { launch { var i = 0 while (true) { try { delay(500L) } catch (e: CancellationException) { println("捕获CancellationException异常") // 抛出异常 throw e } i++ println("协程1 i= $i") } } launch { var i = 0 while (true) { delay(500L) i++ println("协程2 i= $i") } } } delay(2000L) parentJob.cancel() parentJob.join() println("end") } /* 输出信息: 协程1 i= 1 协程2 i= 1 协程2 i= 2 协程1 i= 2 协程2 i= 3 协程1 i= 3 捕获CancellationException异常 end */
准则四:不要用try-catch直接包裹launch、async
问题:try-catch不起作用
fun main() = runBlocking { try { launch { delay(100L) 1 / 0 //产生异常 } } catch (e: ArithmeticException) { println("捕获:$e") } delay(500L) println("end") } /* 输出信息: Exception in thread "main" java.lang.ArithmeticException: / by zero */
原因:协程的代码执行顺序与普通程序不一样,当协程执行 1 / 0
时,程序实际已经跳出 try-catch 的作用域了,所以直接使用 try-catch 包裹 launch、async 是没有任何效果的。
解决:调整作用域
可以将 try-catch 移动到协程体内部,这样可以捕获到异常了。
fun main() = runBlocking { launch { delay(100L) try { 1 / 0 //产生异常 } catch (e: ArithmeticException) { println("捕获异常:$e") } } delay(500L) println("end") } /* 输出信息: 捕获异常:java.lang.ArithmeticException: / by zero end */
准则五:灵活使用SurpervisorJob
问题:子Job发生异常影响其他子Job
fun main() = runBlocking { launch { launch { 1 / 0 delay(100L) println("hello world 111") } launch { delay(200L) println("hello world 222") } launch { delay(300L) println("hello world 333") } } delay(1000L) println("end") } /* 输出信息: Exception in thread "main" java.lang.ArithmeticException: / by zero */
原因:使用普通 Job 时,当子Job发生异常时,会导致 parentJob 取消,从而导致其他子Job也受到牵连,这也是协程结构化的体现。
解决:使用 SupervisorJob
SurpervisorJob 是 Job 的子类,SurpervisorJob 是一个种特殊的 Job,可以控制异常的传播范围,当子Job发生异常时,其他的子Job不会受到影响。
将 parentJob 改为 SupervisorJob。
fun main() = runBlocking { val scope = CoroutineScope(SupervisorJob()) scope.launch { 1 / 0 delay(100L) println("hello world 111") } scope.launch { delay(200L) println("hello world 222") } scope.launch { delay(300L) println("hello world 333") } delay(1000L) println("end") } /* 输出信息: Exception in thread "DefaultDispatcher-worker-1 @coroutine#2" java.lang.ArithmeticException: / by zero hello world 222 hello world 333 end */
解决:使用 supervisorScope
supervisorScope 底层依然使用的是 SupervisorJob。
fun main() = runBlocking { supervisorScope { launch { 1 / 0 delay(100L) println("hello world 111") } launch { delay(200L) println("hello world 222") } launch { delay(300L) println("hello world 333") } } delay(1000L) println("end") } /* 输出信息: Exception in thread "main" java.lang.ArithmeticException: / by zero hello world 222 hello world 333 end */
准则六:使用 CoroutineExceptionHandler 处理复杂结构的协程异常
问题:复杂结构的协程异常
fun main() = runBlocking { val scope = CoroutineScope(coroutineContext) scope.launch { async { delay(100L) } launch { delay(100L) launch { delay(100L) 1 / 0 } } delay(100L) } delay(1000L) println("end") } /* 输出信息: Exception in thread "main" java.lang.ArithmeticException: / by zero */
原因:模拟一个复杂的协程嵌套场景,开发人员很难在每一个协程体中写 try-catch,为了捕获异常,可以使用 CoroutineExceptionHandler。
解决:使用CoroutineExceptionHandler
使用 CoroutineExceptionHandler 处理复杂结构的协程异常,它只能在顶层协程中起作用。
fun main() = runBlocking { val myCoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable -> println("捕获异常:$throwable") } val scope = CoroutineScope(coroutineContext + Job() + myCoroutineExceptionHandler) scope.launch { async { delay(100L) } launch { delay(100L) launch { delay(100L) 1 / 0 } } delay(100L) } delay(1000L) println("end") } /* 输出信息: 捕获异常:java.lang.ArithmeticException: / by zero end */
总结
- 准则一:协程的取消需要内部的配合。
- 准则二:不要轻易打破协程的父子结构。协程的优势在于结构化并发,他的许多特性都是建立在这之上的,如果打破了它的父子结构,会导致协程无法按照预期执行。
- 准则三:捕获 CancellationException 异常后,需要考虑是否重新抛出来。协程是依赖 CancellationException 异常来实现结构化取消的,捕获异常后需要考虑是否重新抛出来。
- 准则四:不要用 try-catch 直接包裹 launch、async。协程代码的执行顺序与普通程序不一样,直接使用 try-catch 可能不会达到预期效果。
- 准则五:使用 SupervisorJob 控制异常传播范围。SupervisorJob 是一种特殊的 Job,可以控制异常的传播范围,不会受到子协程中的异常而取消自己。
- 准则六:使用 CoroutineExceptionHandler 捕获异常。当协程嵌套层级比较深时,可以在顶层协程中定义 CoroutineExceptionHandler 捕获整个作用域的所有异常。
到此这篇关于Kotlin 协程的异常处理的文章就介绍到这了,更多相关Kotlin 协程的异常处理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!