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 协程设计也遵循该编程范式。
相关推荐
太空漫步111 小时前
android社畜模拟器
android
海绵宝宝_4 小时前
【HarmonyOS NEXT】获取正式应用签名证书的签名信息
android·前端·华为·harmonyos·鸿蒙·鸿蒙应用开发
凯文的内存6 小时前
android 定制mtp连接外设的设备名称
android·media·mtp·mtpserver
天若子6 小时前
Android今日头条的屏幕适配方案
android
林的快手7 小时前
伪类选择器
android·前端·css·chrome·ajax·html·json
望佑8 小时前
Tmp detached view should be removed from RecyclerView before it can be recycled
android
xvch10 小时前
Kotlin 2.1.0 入门教程(二十四)泛型、泛型约束、绝对非空类型、下划线运算符
android·kotlin
人民的石头14 小时前
Android系统开发 给system/app传包报错
android
yujunlong391914 小时前
android,flutter 混合开发,通信,传参
android·flutter·混合开发·enginegroup
rkmhr_sef14 小时前
万字详解 MySQL MGR 高可用集群搭建
android·mysql·adb