Kotlin | 关于协程异常处理,你想知道的都在这里(下)

简介: 关于协程的异常处理,一直以来都不是一个简单问题。因为涉及到了很多方面,包括 异常的传递 ,结构化并发下的异常处理 ,异常的传播方式 ,不同的Job 等,所以常常让很多(特别是刚使用协程的,也不乏老手)同学摸不着头脑。

答案是: 不会生效

Tips: 如果你不是很理解 async 的 CoroutineContext 里此时为什么要加 SupervisorJob ,请看下面,会再做解释。

你可能会想,这还不简单吗,上面不是已经提过了,如果根协程或者scope中没有设置 CoroutineExceptionHandler,异常会被直接抛出,所以这里肯定异常了啊。


如果你这样想了,恭喜回答正确~ 👏

那该怎么改一下上述示例呢?

scope 初始化时 或者 根协程里 加上 CoroutineExceptionHandler,或者直接 async 里面 try catch 都可以。那还有没有其他方式呢?

此处停留10s 思考,loading…

如果你还记得我们最开始说过的异常的 传播形式 ,就会知道,对于 async 这种,在其异常时,其会主动向用户暴漏,而不是优先向上传递。

也就是说,我们直接可以在 await() 时 try Catch 。代码如下:

scope.launch {
    val asyncA = async(SupervisorJob()){}
    val asyncB = async(SupervisorJob()){}
    val resultA = kotlin.runCatching { asyncA.await() }
    val resultB = kotlin.runCatching { asyncB.await() }
}

runCatching 是 kotlin 中对于 tryCatch 的一种包装,其会将结果使用 Result 类进行包装,从而让我们能更直观的处理结果,从而更加符合 kotlin 的语法习惯。

Tips

为什么上述 async 里要添加 SupervisorJob() ,这里再做一个解释。

val scope = CoroutineScope(Job())
scope.launch {
    val asyncA = async(SupervisorJob()) { throw RuntimeException()}
    val asyncB = async xxx
}

因为 async 时内部也是新的作用域,如果 async 对应的是根协程,那么我们可以在 await() 时直接捕获异常。怎么理解呢?

如下示例:

val scope = CoroutineScope(Job())
// async 作为根协程
val asyncA = scope.async { throw NullPointerException() }
val asyncB = scope.async { }
scope.launch {
    // 此时可以直接tryCatch
    kotlin.runCatching {
        asyncA.await()
        asyncB.await()
    }
}

但如果 async 其对应的不是根协程(即不是 scope直接.async ),则会先将异常传递给父协程,从而导致异常没有在调用处暴漏,我们的tryCatch 自然也就无法拦截。如果此时我们为其增加 SupervisorJob() ,则标志着其不会主动传递异常,而是由该协程自行处理。所以我们可以在调用处(await()) 捕获。


相关扩展

supervisorScope

官方解释如下:使用 SupervisorJob 创建一个 CoroutineScope 并使用此范围调用指定的挂起块。提供的作用域从外部作用域继承其coroutineContext ,但用 SupervisorJob 覆盖上下文的 Job 。一旦给定块及其所有子协程完成,此函数就会返回。

通俗点就是,我们帮你创建了一个 CoroutineScope ,初始化作用域时,使用 SupervisorJob 替代默认的Job,然后将其的作用域扩展至外部调用。如下代码所示:

val scope = CoroutineScope(CoroutineExceptionHandler { _, _ -> })
scope.launch() {
    supervisorScope {
        // launch A ❎
        launch(CoroutineName("A")) {
            delay(10)
            throw RuntimeException()
        }
        // launch B 👍
        launch(CoroutineName("B")) {
            delay(100)
            Log.e("petterp", "正常执行,我不会收到影响")
        }
    }
}

当 supervisorScope 里的所有子协程执行完成时,其就会正常退出作用域。

需要注意的是,supervisorScope 内部的 Job 为 SupervisorJob ,所以当作用域中子协程异常时,异常不会主动层层向上传递,而是由子协程自行处理,所以意味着我们也可以为子协程增加 CoroutineExceptionHandler 。如下所示:

当子协程异常时,因为我们使用了 supervisorScope ,所以异常此时不会主动传递给外部,而是由子类自行处理。

当我们在内部 launch 子协程时,其实也就是类似 scope.launch ,所以此时子协程A相也就是根协程,所以我们使用 CoroutineExceptionHandler 也可以正常拦截异常。但如果我们子协程不增加 CoroutineExceptionHandler ,则此时异常会被supervisorScope 抛出,然后被外部的 CoroutineExceptionHandler 拦截(也就是初始化scope作用域时使用的 ExceptionHandler)。

相应的,与 supervisorScope 相似的,还有一个 coroutineScope ,下面我们也来说一下这个。


coroutineScope

其主要用于并行分解协程子任务时而使用,当其范围内任何子协程失败时,其所有的子协程也都将被取消,一旦内部所有的子协程完成,其也会正常返回。

如下示例:

当子协程A 异常未被捕获时,此时 子协程B 和整个 协程作用域 都将被异常取消,此时异常将传递到顶级 CoroutineExceptionHandler

场景推荐

严格意义上来说,所有异常都可以用 tryCatch 去处理,只要我们的处理位置得当。但这并不是所有方式的最优解,特别是如果你想更优雅的处理异常时,此时就可以考虑 CoroutineExceptionHandler 。下面我们通过实际需求来举例,从而体会异常处理的的一些实践。

什么时候该用 SupervisorJob ,什么时候该用 Job?

引用官方的一句话就是:想要避免取消操作在异常发生时被传播,记得使用 SupervisorJob ;反之则使用 Job。

对于一个普通的协程,如何处理我的异常?

对于一个普通的协程,你可以在其协程作用域内使用 tryCatch(runCatching) ,如果其是根协程,你也可以使用 CoroutineExceptionHandler 作为最后的拦截手段 ,如下所示:

val scope = CoroutineScope(Job())
scope.launch {
    runCatching { }
}
scope.launch(CoroutineExceptionHandler { _, throwable -> }) {  
}

在某个子协程中,想使用 SupervisorJob 的特性去作为某个作用域去执行?

val scope = CoroutineScope(Job())
scope.launch(CoroutineExceptionHandler { _, _ -> }) {
    supervisorScope {
        launch(CoroutineName("A")) {
            throw NullPointerException()
        }
        launch(CoroutineName("B")) {
            delay(1000)
            Log.e("petterp", "依然会正常执行")
        }
    }
}

SupervisorJob+tryCatch

我们有两个接口 A,B 需要同时请求,当接口A异常时,需要不影响B接口的正常展示,当接口B异常时,此时界面展示异常信息。伪代码如下:

val scope = CoroutineScope(Job())
scope.launch {
    val jobA = async(SupervisorJob()) {
        throw NullPointerException()
    }
    val jobB = async(SupervisorJob()) {
        delay(100)
        1
    }
    val resultA = kotlin.runCatching { jobA.await() }
    val resultB = kotlin.runCatching { jobB.await() }
}

CoroutineExceptionHandler+SupervisorJob

如果你有一个顶级协程,并且需要自动捕获所有的异常,则此时可以选用上述方式,如下所示:

val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
    Log.e("petterp", "自动捕获所有异常")
}
val ktxScope = CoroutineScope(SupervisorJob() + exceptionHandler)
目录
相关文章
|
28天前
|
调度 开发者 UED
Kotlin 中的协程是什么?
【8月更文挑战第31天】
52 0
|
3月前
|
Java Serverless Kotlin
Kotlin中的异常处理
Kotlin中的异常处理
193 1
|
3月前
|
存储 Java 调度
Android面试题之Kotlin 协程的挂起、执行和恢复过程
了解Kotlin协程的挂起、执行和恢复机制。挂起时,状态和上下文(局部变量、调用栈、调度器等)被保存;挂起点通过`Continuation`对象处理,释放线程控制权。当恢复条件满足,调度器重新分配线程,调用`resumeWith`恢复执行。关注公众号“AntDream”获取更多并发知识。
74 2
|
4月前
|
移动开发 Android开发 开发者
构建高效Android应用:Kotlin与协程的完美融合
【5月更文挑战第25天】 在移动开发的世界中,性能和响应性是衡量应用质量的关键指标。随着Kotlin的流行和协程的引入,Android开发者现在有了更强大的工具来提升应用的性能和用户体验。本文深入探讨了Kotlin语言如何与协程相结合,为Android应用开发带来异步处理能力的同时,保持代码的简洁性和可读性。我们将通过实际案例分析,展示如何在Android项目中实现协程,以及它们如何帮助开发者更有效地管理后台任务和用户界面的流畅交互。
|
4月前
|
移动开发 数据库 Android开发
构建高效Android应用:探究Kotlin的协程优势
【5月更文挑战第22天】随着移动开发技术的不断进步,Android平台的性能优化已经成为开发者关注的焦点。在众多提升应用性能的手段中,Kotlin语言提供的协程概念因其轻量级线程管理和异步编程能力而受到广泛关注。本文将深入探讨Kotlin协程在Android开发中的应用,以及它如何帮助开发者构建出更高效、响应更快的应用,同时保持代码的简洁性和可读性。
|
4月前
|
移动开发 Android开发 开发者
构建高效安卓应用:Kotlin 协程的实践指南
【5月更文挑战第18天】 随着移动开发技术的不断进步,安卓平台亟需一种高效的异步编程解决方案来应对日益复杂的应用需求。Kotlin 协程作为一种新兴的轻量级线程管理机制,以其简洁的语法和强大的功能,成为解决这一问题的关键。本文将深入探讨Kotlin协程在安卓开发中的实际应用,从基本概念到高级技巧,为开发者提供一份全面的实践指南,旨在帮助读者构建更加高效、稳定的安卓应用。
|
3月前
|
XML 存储 数据格式
Kotlin Fuel库:图像下载过程中的异常处理
Kotlin Fuel库:图像下载过程中的异常处理
|
4月前
|
移动开发 安全 Android开发
构建高效Android应用:Kotlin与协程的完美结合
【5月更文挑战第17天】 在移动开发领域,性能优化和流畅的用户体验是关键。对于Android平台而言,Kotlin语言凭借其简洁性和功能安全性成为开发的首选。与此同时,协程作为一种新的并发处理方式,在简化异步编程方面展现出巨大潜力。本文将深入探讨如何通过Kotlin语言以及协程技术,提升Android应用的性能和响应能力,并确保用户界面的流畅性。
|
4月前
|
移动开发 数据处理 Android开发
构建高效Android应用:Kotlin的协程与Flow的使用
【5月更文挑战第23天】 在移动开发领域,性能优化和异步编程一直是核心议题。随着Kotlin语言在Android开发中的普及,其提供的协程(coroutines)和流式编程(Flow)功能为开发者带来了革命性的工具,以更简洁、高效的方式处理异步任务和数据流。本文将深入探讨Kotlin协程和Flow在Android应用中的实际应用,以及它们如何帮助开发者编写更加响应迅速且不阻塞用户界面的应用程序。我们将通过具体案例分析这两种技术的优势,并展示如何在现有项目中实现这些功能。
|
3月前
|
Go Python
使用python实现一个用户态协程
【6月更文挑战第28天】本文探讨了如何在Python中实现类似Golang中协程(goroutines)和通道(channels)的概念。文章最后提到了`wait_for`函数在处理超时和取消操作中的作
40 1
使用python实现一个用户态协程