Kotlin 协程的异常处理准则

 更新时间:2024年01月10日 10:32:43   作者:xiangxiongfly915  
协程是互相协作的程序,协程是结构化的,正是因为协程的这两个特点,导致它和 Java 的异常处理机制不一样,这篇文章重点给大家介绍Kotlin 协程的异常处理准则,感兴趣的朋友一起看看吧

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 协程的异常处理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • android ListView和GridView拖拽移位实现代码

    android ListView和GridView拖拽移位实现代码

    有些朋友对android中ListView和GridView拖拽移位功能的实现不是很了解,接下来将详细介绍,需要了解的朋友可以参考下
    2012-12-12
  • Android 7.0 运行时权限弹窗问题的解决

    Android 7.0 运行时权限弹窗问题的解决

    这篇文章主要介绍了Android 7.0 运行时权限弹窗问题的解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-03-03
  • Android图片翻转动画简易实现代码

    Android图片翻转动画简易实现代码

    Android图片翻转动画效果如何实现,本文将给你一个惊喜,实现代码已经列出,需要的朋友可以参考下
    2012-11-11
  • Android四大组件:Activity/Service/Broadcast/ContentProvider作用示例

    Android四大组件:Activity/Service/Broadcast/ContentProvider作用示例

    Android是一种基于Linux,自由及开放源代码的操作系统,Android分为四个层,从高层到底层分别是应用程序层、应用程序框架层、系统运行库层和Linux内核层,Android有四大基本组件:Activity、Service服务、BroadcastReceiver广播接收器、Content Provider内容提供者
    2023-11-11
  • Android使用美团多渠道打包方案详解

    Android使用美团多渠道打包方案详解

    这篇文章主要介绍了Android使用美团多渠道打包方案详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-11-11
  • 详解Android XML中引用自定义内部类view的四个why

    详解Android XML中引用自定义内部类view的四个why

    本篇文章主要介绍了详解Android XML中引用自定义内部类view,小编觉得挺不错的,现在分享给大家,也给大家做个参考。
    2016-12-12
  • GreenDao 3.2.0 的基本使用

    GreenDao 3.2.0 的基本使用

    本文主要对GreenDao 3.2.0 的基本使用进行详细介绍。具有一定的参考价值,下面跟着小编一起来看下吧
    2017-01-01
  • Android蓝牙通信之搜索蓝牙设备

    Android蓝牙通信之搜索蓝牙设备

    这篇文章主要介绍了Android蓝牙通信之搜索蓝牙设备,需要的朋友可以参考下
    2017-09-09
  • Android程序锁的实现以及逻辑

    Android程序锁的实现以及逻辑

    本篇文章主要是介绍Android程序锁的实现以及逻辑,它的目的是可以给程序加锁,上过锁的程序可以解锁,有兴趣的朋友可以了解一下。
    2016-10-10
  • Flutter Set存储自定义对象时保证唯一的方法详解

    Flutter Set存储自定义对象时保证唯一的方法详解

    在Flutter中,Set和List是两种不同的集合类型,List中存储的元素可以重复,Set中存储的元素不可重复,如果想在Set中存储自定义对象,需要确保对象的唯一性,那么如何保证唯一,接下来小编就给大家详细的介绍一下
    2023-11-11

最新评论