侧边栏壁纸
博主头像
Z同学博主等级

工作磨平激情前,坚持技术的热忱。 欢迎光临Z同学的技术小站。 分享最新的互联网知识。

  • 累计撰写 274 篇文章
  • 累计创建 55 个标签
  • 累计收到 74 条评论

Kotlin 协程 异常处理 逻辑的介绍

Z同学
2022-01-22 / 0 评论 / 1 点赞 / 193 阅读 / 6,402 字
温馨提示:
本文最后更新于 2022-01-22,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

前言

我们在上面介绍了很多关于协程方面的知识了。Kotlin 协程 学习笔记全集

这篇介绍协程中的各种异常处理以及监督。

我们知道,在取消协程的时候会触发CancellationException 默认情况下,该异常会被协程自动忽略。我们通常不用关心这个取消时触发的异常的捕获与处理。

那么,在协程的子协程中或者多个子协程中抛出的其他的异常会怎么处理?Kotlin又会有什么样的反应呢?

本篇主要就是学习这方面的知识。

1. 异常传播

我们在创建协程的时候有两种构建器模式,一种是默认自动处理(Launchactor),一种是向用户暴露异常

asyncproduce)。当我们使用这些构建一个根协程的时候(即该协程不是另外一个协程的子协程),前者会将异常视为未捕获异常,类似java中的Thread.uncaughtExceptionHandler。后者则会依赖用户来最终消费异常(例如通过await或者receiver等)。

示例:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = GlobalScope.launch { // launch 根协程
        println("正在尝试在主协程下创建一个异常")
        throw IndexOutOfBoundsException() // 我们将在控制台打印 Thread.defaultUncaughtExceptionHandler
    }
    job.join()
    println("加载一个失败的job")
    val deferred = GlobalScope.async { // async 根协程
        println("尝试在异步模式下创建一个异常")
        throw ArithmeticException() // 没有打印任何东西,依赖用户去调用等待
    }
    try {
        deferred.await()
        println("这一步 不会被打印")
    } catch (e: ArithmeticException) {
        println("捕获到的ArithmeticException异常")
    }
}
//输出
正在尝试在主协程下创建一个异常
Exception in thread "DefaultDispatcher-worker-2" java.lang.IndexOutOfBoundsException
	at com.zinyan.general.ListTempKt$main$1$job$1.invokeSuspend(ListTemp.kt:8)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
加载一个失败的job
尝试在异步模式下创建一个异常
捕获到的ArithmeticException异常

我们结合输出的日志,可以领悟到协程的传播逻辑了。

2. CoroutineExceptionHandler

我们知道,协程取消的时候会有默认的异常。这个异常是未捕获异常。

在该协程中的CoroutineExceptionHandler 上下文元素可以被用于这个协程通用的catch块。

它类似Thread.uncaughtExceptionHandler。我们无法从CoroutineExceptionHandler的异常中恢复。

当调用处理者的时候,协程已经完成并带有相应的异常。通常情况下,该处理者用于记录异常,显示某种错误消息,终止和重启应用程序。

(可能不太能够理解,没关系我们先大概明白有这么一个东西就可以了)

示例:

import kotlinx.coroutines.*

fun main() = runBlocking {
    joinAll(job, deferred)
}
val handler = CoroutineExceptionHandler { _, exception ->
    println("CoroutineExceptionHandler got $exception")
}
val job = GlobalScope.launch(handler) { // 根协程,运行在 GlobalScope 中
    throw AssertionError()
}
val deferred = GlobalScope.async(handler) { // 同样是根协程,但使用 async 代替了 launch
    throw ArithmeticException() // 没有打印任何东西,依赖用户去调用 deferred.await()
}
//输出
CoroutineExceptionHandler got java.lang.AssertionError

3.取消与异常

协程内部使用 CancellationException 来进行取消,这个异常会被所有的处理者默认忽略,所以那些可以被 catch 代码块捕获的异常仅仅应该被用来作为额外调试信息的资源。 当一个协程通过Job.cancel()取消的时候,它会终止它本身。

示例:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        val child = launch {
            try {
                delay(Long.MAX_VALUE)
            } finally {
                println("子协程-取消")
            }
        }
        yield()
        println("子协程被取消中")
        child.cancel()
        child.join()
        yield()
        println("父协程没有被取消")
    }
    job.join()
}
//输出
子协程被取消中
子协程-取消
父协程没有被取消

如果一个协程碰见了CancellationException以外的异常,它会使用该异常取消掉它的父协程。

这整个取消过程无法被覆盖修改。它被用于为结构化的并发提供稳定的协程层级结构。

当父协程的所有子协程都结束后,原始的异常才会被父协程处理。

示例:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception")
    }
    val job = GlobalScope.launch(handler) {
        launch { // 第一个子协程
            try {
                delay(Long.MAX_VALUE)
            } finally {
                withContext(NonCancellable) {
                    println("子项被取消,但在所有子项终止之前不会处理异常")
                    delay(100)
                    println("第一个子项完成了它的逻辑")
                }
            }
        }
        launch { // 第二个子协程
            delay(10)
            println("第二个子项抛出了一个异常")
            throw ArithmeticException()
        }
    }
    job.join()
}
//输出
第二个子项抛出了一个异常
子项被取消,但在所有子项终止之前不会处理异常
第一个子项完成了它的逻辑
CoroutineExceptionHandler got java.lang.ArithmeticException

4. 异常聚合

当协程的多个子协程都因为异常而失败的时候,一般规则是:取第一个异常

因此将会处理第一个异常,在第一个异常之后发生的其他的所有异常都将会作为被抑制的异常并绑定到第一个异常下。

示例:

import kotlinx.coroutines.*
import java.io.*

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler 获取: $exception 抑制 ${exception.suppressed.contentToString()}")
    }
    val job = GlobalScope.launch(handler) {
        launch {
            try {
                delay(Long.MAX_VALUE) // 当另一个同级的协程因 IOException  失败时,它将被取消
            } finally {
                throw ArithmeticException() // 第二个异常
            }
        }
        launch {
            delay(100)
            throw IOException() // 首个异常
        }
        delay(Long.MAX_VALUE)
    }
    job.join()
}
//输出
CoroutineExceptionHandler 获取: java.io.IOException 抑制 [java.lang.ArithmeticException]

我们可以看到第一个异常是io异常。而之后的ArithmeticException 将会被抑制。

5. 监督

取消时在协程的整个层次结构中传播的双向关系,我们如果需要单向取消的情况该如何呢?

这种需求的示例场景就是:如果在作用域内定义了一个UI组件。其中任何一个UI的子作业执行失败了,它并不一定要取消整个UI组件。但是如果UI组件被销毁了,由于它的结果不再被需要,那么它有必要使所有的子作业执行失败。

另外的示例是:一个服务进程孵化了一些子作业并且需要监督它们的执行,追踪它们的故障并在这些子作业执行失败的时候重启。

这就是Kotlin中监督的作用了。

5.1 监督作业-SupervisorJob

可以使用SupervisorJob 监督作业。它类似于普通的Job,而唯一不同的地方在于它的取消只会向下传播。

示例:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val supervisor = SupervisorJob()
    with(CoroutineScope(coroutineContext + supervisor)) {
        // 启动第一个子作业——这个示例将会忽略它的异常(不要在实践中这么做!)
        val firstChild = launch(CoroutineExceptionHandler { _, _ ->  }) {
            println("第一个子作业,失败了")
            throw AssertionError("第一个子作业被取消")
        }
        // 启动第二个子作业
        val secondChild = launch {
            firstChild.join()
            // 取消了第一个子作业且没有传播给第二个子作业
            println("第一个子作业的取消结果: ${firstChild.isCancelled}, 但是第二个子作业还是处于活动状态")
            try {
                delay(Long.MAX_VALUE)
            } finally {
                // 但是取消了监督的传播
                println("因为主监督对象被关闭,所有第二个子作业也进行取消")
            }
        }
        // 等待直到第一个子作业失败且执行完成
        firstChild.join()
        println("取消主监督")
        supervisor.cancel()
        secondChild.join()
    }
}
//输出
第一个子作业,失败了
第一个子作业的取消结果: true, 但是第二个子作业还是处于活动状态
取消主监督
因为主监督对象被关闭,所有第二个子作业也进行取消

我们结合输出的结果,可以更方便的理解代码想表达的意义。

5.2 监督作用域

我们知道协程是运行在作用域内的。对于作用域的并发,我们可以通过supervisorScope来替代coroutineScope实现相同的目标。

它只会单向的传播并且当作业自身执行失败的时候,将所有的子作业全部取消。

作业自身也会在所有的子作业执行结束前等待,就像coroutineScope 的功能一样。

示例:

import kotlinx.coroutines.*

fun main() = runBlocking {
    try {
        supervisorScope {
            val child = launch {
                try {
                    println("子作业处于阻塞状态")
                    delay(Long.MAX_VALUE)
                } finally {
                    println("子作业 处于取消状态")
                }
            }
            // 使用 yield 来给我们的子作业一个机会来执行打印
            yield()
            println("我们主动从作用域内 人为抛出一个异常")
            throw AssertionError()
        }
    } catch(e: AssertionError) {
        println("捕获到的所有异常")
    }
}
//输出
子作业处于阻塞状态
我们主动从作用域内 人为抛出一个异常
子作业 处于取消状态
捕获到的所有异常

5.3 监督协程中的异常

我们在处理完Job作业中的异常,作用域中的异常后,我们最后来了解下协程中的异常监督。

常规的作业和监督作业之间的另外一个重要区别就是异常处理。

监督协程中的每个子作业应该通过异常处理机制处理自身的异常。这种差异来自于子作业的执行失败不会传递给它的父作业的事实。

在SupervisorScope内部直接启动的协程使用了设置在它们作用域内的CoroutineExceptionHandler,与父协程的方式相同。

示例:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("获取 CoroutineExceptionHandler  $exception")
    }
    supervisorScope {
        val child = launch(handler) {
            println("子作业主动抛出一个异常")
            throw AssertionError()
        }
        println("scope 正在完成中")
    }
    println("scope 已经完成")
}
//输出
scope 正在完成中
子作业主动抛出一个异常
获取 CoroutineExceptionHandler  java.lang.AssertionError
scope 已经完成

本篇就介绍到这里,只是一个比较粗略的理解。

希望能够给大家一些参考,之后会写更多更详细的内容。大家可以随时关注Z同学

1

评论区