欢迎访问移动开发之家(rcyd.net),关注移动开发教程。移动开发之家  移动开发问答|  每日更新
页面位置 : > > > 内容正文

Kotlin协程的异常处理准则,

来源: 开发者 投稿于  被查看 27755 次 评论:71

Kotlin协程的异常处理准则,


目录
  • Kotlin 协程的异常处理
    • 概述
    • 异常处理六大准则
    • 准则一:协程的取消需要内部配合
      • 问题:cancel不被响应
      • 解决:使用 isActive 判断是否处于活跃状态
    • 准则二:不要打破协程的父子结构
      • 问题:子协程不会跟随父协程一起取消
      • 解决:不破坏父子结构
    • 准则三:捕获 CancellationException 需要重新抛出来
      • 挂起函数可以自动响应协程的取消
      • 问题:捕获 CancellationException 导致崩溃
      • 解决:需要重新抛出
    • 准则四:不要用try-catch直接包裹launch、async
      • 问题:try-catch不起作用
      • 解决:调整作用域
    • 准则五:灵活使用SurpervisorJob
      • 问题:子Job发生异常影响其他子Job
      • 解决:使用 SupervisorJob
      • 解决:使用 supervisorScope
    • 准则六:使用 CoroutineExceptionHandler 处理复杂结构的协程异常
      • 问题:复杂结构的协程异常
      • 解决:使用CoroutineExceptionHandler
  • 总结

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

    您可能感兴趣的文章:
    • Kotlin 协程 supervisorScope {} 运行崩溃解决方法
    • Kotlin 协程与挂起函数及suspend关键字深入理解
    • Kotlin启动协程的三种方式示例详解
    • Kotlin协程Channel特点及使用细节详解
    • Kotlin协程launch启动流程原理详解
    • Kotlin协程Job生命周期结构化并发详解

    用户评论