kotlin 协程:异常传递与处理

相关文章:kotlin协程:一文搞懂各种概念

前言

本文全面解释协程的异常传递机制以及处理方式,需要一定的协程基础。摆脱只会使用 try catch 的尴尬,以更优雅和更灵活的方式处理异常。

异常传递

Job

对于普通 Job 来说,异常的传递是双向的,即异常会向子协程和父协程传播,流程为:

  • 当前协程出现异常
  • cancel 子协程
  • 等待子协程 cancel 完成,cancel 自己
  • 传递给父协程并循环前面步骤

可见,异常会自下而上地传播,任一协程发生异常会影响到整个协程树。

如下代码中,child 1出现异常后,child 3、child 2、child 0 依次会被取消。

kotlin 复制代码
CoroutineScope(Job()).launch { // child 0
    launch {// child 1
        launch { // child 3
            delay(1000)
            println("child 3")
        }
        throw Exception("test")
    }
    launch { // child 2
        delay(1000)
        println("child 2")
    }
    delay(1000)
    println("child 0")
}.join()

SupervisorJob

如果不希望协程内的异常向上传播或影响同级协程。可以使用 SupervisorJob

SuperVisorJob 可以使子协程的异常向上传播到 SupervisorJob 层时不被处理,即异常向上传播在 SupervisorJob 处终止。

如上图所示,SupervisorJobScope 的任一直接子协程 发生异常都不会影响其他直接子协程,更不会向上传播影响父协程。

注意,父协程异常依然会导致子协程取消,这点和 Job 一致。

kotlin 复制代码
CoroutineScope(Job()).launch { // child 0
    val supervisorJob = SupervisorJob()
    launch(supervisorJob) {// child 1
        launch { // child 3
            delay(1000)
            println("child 3")
        }
        throw Exception("test")
    }
    launch(supervisorJob) { // child 2
        delay(1000)
        println("child 2")
    }
    delay(1000)
    println("child 0")
}.join()

还是上面的例子,使用 SupervisorJob 作为 child 1 和 child 2 的父 Job,这样 child 2 中的异常只能向下影响 child 3 ,避免了异常影响到 child 0 和 child 2。

也可以使用 supervisorScope ,它是一个自带了 SupervisorJob 的协程作用域,效果是一样的:

kotlin 复制代码
CoroutineScope(Job()).launch { // child 0
    supervisorScope {
        launch {// child 1
            launch { // child 3
                delay(1000)
                println("child 3")
            }
            throw Exception("test")
        }
        launch { // child 2
            delay(1000)
            println("child 2")
        }
    }
    delay(1000)
    println("child 0")
}.join()

不同的是,supervisorScope 是挂起方法,会挂起协程直至所有子协程执行完成。

注意,易错 💥

下面代码中,谁是 Child1 的父协程?

kotlin 复制代码
val scope = CoroutineScope(Job())
scope.launch(SupervisorJob()) {
   launch {
        // Child 1
    }
    launch {
        // Child 2
    }
}

Child 的父协程类型是 Job ,并不是 SupervisorJob

scope.launch 会创建一个新 Job,该 Job 的父 Job 是 launch 时指定的,所以 SupervisorJob 是新 Job 的父 Job ,而 scope 创建时传入的 Job 被 SupervisorJob 覆盖,因而协程关系为:

SupervisorJob -〉Job -〉(child1、child2),因而上述代码中,SupervisorJob 没有起到该有的作用。

鉴于上述原因,可以:

kotlin 复制代码
val scope = CoroutineScope(SupervisorJob())
scope.launch {
    // Child 1
}
scope.launch {
    // Child 2
}

也可以使用 supervisorScope 达到预期效果:

scss 复制代码
supervisorScope {
    launch { // Child 1
        throw IllegalArgumentException()
    }
    launch { // Child 2
        delay(1000)
        println("child 2 run over") //可顺利执行完成
    }
    delay(1000)
    println("Job: ${coroutineContext[Job]?.javaClass}")
}

打印结果:

arduino 复制代码
Exception in thread "Test worker @coroutine#2" java.lang.IllegalArgumentException
Job: class kotlinx.coroutines.SupervisorCoroutine
child 2 run over

不管是什么 Job,异常传递如果不做处理,最终会到达线程的 UncaughtExceptionHandler ,如果是 JVM 则输出在控制台,如果是 Android 没设置 UncaughtExceptionHandler 则会出现 app 崩溃(如果是主线程则一定崩溃)。

异常处理

对于不同协程构造器,异常的处理方式不同 。分别介绍 launchasync 情况下的异常处理。

Launch

  • try catch

    launch 方式启动的协程,异常会在发生时立刻抛出,使用 try catch 就可以将协程中的异常捕获。如:

    kotlin 复制代码
    scope.launch {
        try {
            codeThatCanThrowExceptions()
        } catch(e: Exception) {
            // Handle exception
        }
    }

    try catch 整个协程也是可以的:

    kotlin 复制代码
    try {
        coroutineScope {
            codeThatCanThrowExceptions()
        }
    } catch (t: Throwable) {
        // Handle exception
    }

    注意,这样是不可以的 ❌ :

    scss 复制代码
    try {
        CoroutineScope().launch {
            codeThatCanThrowExceptions()
        }
    } catch (t: Throwable) {
        // Handle exception
    }

    因为 launch 不是挂起函数。

  • CoroutineExceptionHandler

    除了使用 try catch ,更推荐使用 CoroutineExceptionHandler 对异常进行统一处理。需要注意的是异常会层层代理到根协程,所以 CoroutineExceptionHandler 只能在根协程中才能生效。

    比如这样 ✅ :

    kotlin 复制代码
    CoroutineScope(Job()).launch(CoroutineExceptionHandler { coroutineContext, throwable ->
        println("catch ex successfully")
    }) {
        throw RuntimeException()
    }

    或者这样 ✅ ,根协程会继承该 CoroutineExceptionHandler

    kotlin 复制代码
    CoroutineScope(CoroutineExceptionHandler { coroutineContext, throwable ->
        println("catch ex successfully")
    }).launch() {
        throw RuntimeException()
    }

    而这样是不对的 ❌ :

    kotlin 复制代码
    coroutineScope {
        launch(CoroutineExceptionHandler { coroutineContext, throwable ->
            println("catch ex failed")
        }) {
            throw RuntimeException()
        }
    }

    子协程异常会代理给父协程,一直向上传递直到根协程,如果找到 CoroutineExceptionHandler 则处理,否则走 UncaughtExceptionHandler 。可见,其他子协程中的 CoroutineExceptionHandler 不会起到作用。

    猜猜被谁捕获?:

    kotlin 复制代码
    CoroutineScope(Job() + CoroutineExceptionHandler { coroutineContext, throwable ->
        println("catch ex in scope")
    }).launch(CoroutineExceptionHandler { coroutineContext, throwable -> 
        println("catch ex in top Coroutine")
    }) {
        throw RuntimeException()
    }

    结论:scope 中的 CoroutineExceptionHandler 会覆盖。

    这种将异常代理给父协程的行为可以被 SupervisorJob 改变,将异常交给子协程自己处理:

    kotlin 复制代码
    supervisorScope {
        launch(CoroutineExceptionHandler { coroutineContext, throwable ->
            println("catch ex successfully")
        }) {
            throw RuntimeException()
        }
    }

    异常传递到 SupervisorJob 处停止,交由 SupervisorJob 的直接子协程处理,这时 CoroutineExceptionHandler 是生效的。

    根协程:由 scope 直接调用 launchasync 开启的协程。

Async

  • try catch

    async 开启的协程为根协程,或 SupervisorJob 的直接子协程时,异常在调用 await 时抛出,使用 try catch 可以捕获异常:

    kotlin 复制代码
    /**
     * async 开启的协程为根协程
     */
    fun main() = runBlocking {
        val deferred = GlobalScope.async {
            throw Exception()
        }
        try {
            deferred.await() //抛出异常
        } catch (t: Throwable) {
            println("捕获异常:$t")
        }
    }
    kotlin 复制代码
    /**
     * async 开启的协程为 SupervisorJob 的直接子协程
     */
    fun main() = runBlocking {
        supervisorScope {
            val deferred = async {
                throw Exception()
            }
            try {
                deferred.await() //抛出异常
            } catch (t: Throwable) {
                println("捕获异常:$t")
            }
        }
    }
    kotlin 复制代码
    /**
     * async 开启的协程为 SupervisorJob 的直接子协程
     */
    CoroutineScope(Job()).launch {
        val deferred = async(SupervisorJob()) {
            throw Exception()
        }
        try {
            deferred.await() //抛出异常
        } catch (t: Throwable) {
            println("捕获异常:$t")
        }
    }
  • CoroutineExceptionHandler

    当:

    " async 开启的协程为根协程 或 supervisorScope 的直接子协程"

    的条件不成立时,异常会在发生时立刻抛出并传播,对 await 进行 try catch 就不起作用了:

    kotlin 复制代码
    fun main(): Unit = runBlocking {
        supervisorScope {
            launch {
                val deferred = async { throw Exception() } //抛出异常
                try {
                    deferred.await()
                } catch (e: Exception) {
                    println("catch ex")
                }
                delay(1000)
                println("done")
            }
        }
    }

    控制台中虽然打印了 "catch ex",但未能打印 "done",这表明异常传递到了父协程,父协程被取消。至于为什么还能打印 "catch ex",是因为 deferred.await() 会抛出异常,async 中的异常总会在 await 时抛出。如果在 await 前加个 delay,那么就看不到 "catch ex" 了,因为 await 被取消了。

    可以使用 CoroutineExceptionHandler 进行处理。CoroutineExceptionHandler 只在根协程和 SupervisorJob 的直接子协程有效。因此需要在 launch 开启的父协程进行处理:

    kotlin 复制代码
    /**
     * async 开启的协程非 supervisorScope 的直接子协程,异常会直接抛出,
     * try catch await 无效,但可由 CoroutineExceptionHandler 处理
     */
    fun main(): Unit = runBlocking {
        supervisorScope {
            launch(CoroutineExceptionHandler { coroutineContext, throwable ->
                println("CoroutineExceptionHandler 捕获异常:$throwable")
            }) {
                val deferred = async { throw Exception() } //抛出异常
                deferred.await()
            }
        }
    }
    kotlin 复制代码
    /**
     * async 开启的协程非根协程,异常会直接抛出,
     * try catch await 无效,但可由 CoroutineExceptionHandler 处理
     */
    fun main(): Unit = runBlocking {
        CoroutineScope(Job()).launch(CoroutineExceptionHandler { coroutineContext, throwable ->
            println("CoroutineExceptionHandler 捕获异常:$throwable")
        }) {
            val deferred = async { throw Exception() } //抛出异常
            deferred.await()
        }.join()
    }

总的来说,不管是 launch 还是 async,使用 CoroutineExceptionHandler 的规则都是一致的,也更不易出错,推荐使用。

Cancellation 和 异常

CancellationException 总会被 CoroutineExceptionHandler 忽略,但能被 try catch 捕获,ok,又多了个使用 CoroutineExceptionHandler 的理由。

kotlin 复制代码
fun main() = runBlocking {
    CoroutineScope(Job()).launch(CoroutineExceptionHandler { coroutineContext, throwable ->
        println("CoroutineExceptionHandler 捕获异常:$throwable")
    }) {
        println("运行了")
        cancel()
        try {
            delay(1000)
        } catch (throwable: Throwable) {
            println("try catch 捕获异常:$throwable")
            throw throwable
        }
    }.join()
}

运行结果:

arduino 复制代码
运行了
try catch 捕获异常:kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@54cda7ca

异常聚合

如果一个协程抛出不止一个异常,那么则以第一个抛出的异常为主,其他异常作为 suppressed 异常依附在主异常中。

源码

关于异常的传播控制逻辑可参见 JobSupport.kt 类的 notifyCancellingchildCancelled 方法,SupervisorJob 就是复写了 childCancelled 方法才阻止了异常的向上传播。

结构化并发

它是一种编程范式,旨在通过结构化的方式使并发编程 更清晰明确、更高质量、更易维护。

其核心有几点:

  • 通过把多线程任务进行结构化的包装,使其具有明确的开始和结束点,并确保其孵化出的所有任务在退出前全部完成。
  • 这种包装允许结构中线程发生的异常能够传播至结构顶端的作用域,并且能够被该语言原生异常机制捕获。

kotlin 协程设计中的协程关系、执行顺序、异常传播/处理 都符合结构化并发。结构化并发明确了并发任务什么时候开始,什么时候结束,异常如何传播,通过控制顶层结构具柄就可实现整个并发结构的取消、异常处理,使复杂的并发问题简单、清晰、可控。可以说,结构化并发大大降低了并发编程的难度。

总结

  • 协程中未捕获的异常总会向下取消子协程,向上传递异常,体现了结构化并发的特点。
  • SupervisorJob 可以阻止异常继续向上传播,并将异常交给子协程处理。
  • launch 启动的协程在异常发生时总是立刻抛出,可以由 try catch 捕获,也可以使用 CoroutineExceptionHandler 处理,注意 handler 使用的位置。
  • async 启动的协程在其为根协程或 supervisorScope 的直接子协程时,异常会在 async 内部捕获,当 deferred 对象调用 await 时抛出,否则也会在异常发生时立刻抛出并传播。前者可以使用 try catch 捕获 await 调用,当然 CoroutineExceptionHandler 也可以,而后者不能通过 try catch 整个 async 代码块或 await 调用捕获异常。
  • 结构化并发大大降低了并发编程的难度,kotlin 协程设计也遵循该编程范式。
相关推荐
花生糖@1 小时前
Android XR 应用程序开发 | 从 Unity 6 开发准备到应用程序构建的步骤
android·unity·xr·android xr
是程序喵呀1 小时前
MySQL备份
android·mysql·adb
casual_clover1 小时前
Android 之 List 简述
android·list
锋风Fengfeng2 小时前
安卓15预置第三方apk时签名报错问题解决
android
User_undefined3 小时前
uniapp Native.js原生arr插件服务发送广播到uniapp页面中
android·javascript·uni-app
程序员厉飞雨4 小时前
Android R8 耗时优化
android·java·前端
小白学大数据5 小时前
高级技术文章:使用 Kotlin 和 Unirest 构建高效的 Facebook 图像爬虫
爬虫·数据分析·kotlin
丘狸尾5 小时前
[cisco 模拟器] ftp服务器配置
android·运维·服务器
van叶~7 小时前
探索未来编程:仓颉语言的优雅设计与无限可能
android·java·数据库·仓颉
Crossoads11 小时前
【汇编语言】端口 —— 「从端口到时间:一文了解CMOS RAM与汇编指令的交汇」
android·java·汇编·深度学习·网络协议·机器学习·汇编语言