[Kotlin Tutorials 22] 协程中的异常处理
协程中的异常处理

Parent-Child关系
如果一个coroutine抛出了异常, 它将会把这个exception向上抛给它的parent, 它的parent会做以下三件事情:
- 取消其他所有的children.
- 取消自己.
- 把exception继续向上传递.
这是默认的异常处理关系, 取消是双向的, child会取消parent, parent会取消所有child.
catch不住的exception
看这个代码片段:
fun main() {
val scope = CoroutineScope(Job())
try {
scope.launch {
throw RuntimeException()
}
} catch (e: Exception) {
println("Caught: $e")
}
Thread.sleep(100)
}
这里的异常catch不住了.
会直接让main函数的主进程崩掉.
这是因为和普通的异常处理机制不同, coroutine中未被处理的异常并不是直接抛出, 而是按照job hierarchy向上传递给parent.
如果把try放在launch里面还行.
默认的异常处理
默认情况下, child发生异常, parent和其他child也会被取消.
fun main() {
println("start")
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
val scope = CoroutineScope(Job() + exceptionHandler)
scope.launch {
println("child 1")
delay(1000)
println("finish child 1")
}.invokeOnCompletion { throwable ->
if (throwable is CancellationException) {
println("Coroutine 1 got cancelled!")
}
}
scope.launch {
println("child 2")
delay(100)
println("child 2 throws exception")
throw RuntimeException()
}
Thread.sleep(2000)
println("end")
}
打印出:
start
child 1
child 2
child 2 throws exception
Coroutine 1 got cancelled!
CoroutineExceptionHandler got java.lang.RuntimeException
end
SupervisorJob
如果有一些情形, 开启了多个child job, 但是却不想因为其中一个的失败而取消其他, 怎么办? 用SupervisorJob.
比如:
val uiScope = CoroutineScope(SupervisorJob())
如果你用的是scope builder, 那么用supervisorScope.
用SupervisorJob改造上面的例子:
fun main() {
println("start")
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
val scope = CoroutineScope(SupervisorJob() + exceptionHandler)
scope.launch {
println("child 1")
delay(1000)
println("finish child 1")
}.invokeOnCompletion { throwable ->
if (throwable is CancellationException) {
println("Coroutine 1 got cancelled!")
}
}
scope.launch {
println("child 2")
delay(100)
println("child 2 throws exception")
throw RuntimeException()
}
Thread.sleep(2000)
println("end")
}
输出:
start
child 1
child 2
child 2 throws exception
CoroutineExceptionHandler got java.lang.RuntimeException
finish child 1
end
尽管coroutine 2抛出了异常, 另一个coroutine还是做完了自己的工作.
SupervisorJob的特点
SupervisorJob把取消变成了单向的, 只能从上到下传递, 只能parent取消child, 反之不能取消.
这样既顾及到了由于生命周期的结束而需要的正常取消, 又避免了由于单个的child失败而取消所有.
viewModelScope的context就是用了SupervisorJob() + Dispatchers.Main.immediate.
除了把取消变为单向的, supervisorScope也会和coroutineScope一样等待所有child执行结束.
在supervisorScope中直接启动的coroutine是顶级coroutine.
顶级coroutine的特性:
- 可以加exception handler.
- 自己处理exception.
比如上面的例子中coroutine child 2可以直接加exception handler.
使用注意事项, SupervisorJob只有两种写法:
- 作为
CoroutineScope的参数传入:CoroutineScope(SupervisorJob()). - 使用
supervisorScope方法.
把Job作为coroutine builder(比如launch)的参数传入是错误的做法, 不起作用, 因为一个新的coroutine总会assign一个新的Job.
异常处理的办法
try-catch
和普通的异常处理一样, 我们可以用try-catch, 只是注意要在coroutine里面:
fun main() {
val scope = CoroutineScope(Job())
scope.launch {
try {
throw RuntimeException()
} catch (e: Exception) {
println("Caught: $e")
}
}
Thread.sleep(100)
}
这样就能打印出:
Caught: java.lang.RuntimeException
对于launch, try要包住整块.
对于async, try要包住await语句.
scope function: coroutineScope()
coroutineScope会把其中未处理的exception抛出来.
相比较于这段代码中catch不到的exception:
fun main() {
val scope = CoroutineScope(Job())
scope.launch {
try {
launch {
throw RuntimeException()
}
} catch (e: Exception) {
println("Caught: $e")
}
}
Thread.sleep(100)
}
没走到catch里, 仍然是主进程崩溃.
这个exception是可以catch到的:
fun main() {
val scope = CoroutineScope(Job())
scope.launch {
try {
coroutineScope {
launch {
throw RuntimeException()
}
}
} catch (e: Exception) {
println("Caught: $e")
}
}
Thread.sleep(100)
}
打印出:
Caught: java.lang.RuntimeException
因为这里coroutineScope把异常又重新抛出来了.
注意这里换成supervisorScope可是不行的.
CoroutineExceptionHandler
CoroutineExceptionHandler是异常处理的最后一个机制, 此时coroutine已经结束了, 在这里的处理通常是报告log, 展示错误等.
如果不加exception handler那么unhandled exception会进一步往外抛, 如果最后都没人处理, 那么可能造成进程崩溃.
CoroutineExceptionHandler需要加在root coroutine上.
这是因为child coroutines会把异常处理代理到它们的parent, 后者继续代理到自己的parent, 一直到root.
所以对于非root的coroutine来说, 即便指定了CoroutineExceptionHandler也没有用, 因为异常不会传到它.
两个例外:
async的异常在Deferred对象中,CoroutineExceptionHandler也没有任何作用.- supervision scope下的coroutine不会向上传递exception, 所以
CoroutineExceptionHandler不用加在root上, 每个coroutine都可以加, 单独处理.
通过这个例子可以看出另一个特性: CoroutineExceptionHandler只有当所有child都结束之后才会处理异常信息.
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
val job = GlobalScope.launch(handler) {
launch { // the first child
try {
delay(Long.MAX_VALUE)
} finally {
withContext(NonCancellable) {
println("Children are cancelled, but exception is not handled until all children terminate")
delay(100)
println("The first child finished its non cancellable block")
}
}
}
launch { // the second child
delay(10)
println("Second child throws an exception")
throw ArithmeticException()
}
}
job.join()
}
输出:
Second child throws an exception
Children are cancelled, but exception is not handled until all children terminate
The first child finished its non cancellable block
CoroutineExceptionHandler got java.lang.ArithmeticException
如果多个child都抛出异常, 只有第一个被handler处理, 其他都在exception.suppressed字段里.
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception with suppressed ${exception.suppressed.contentToString()}")
}
val job = GlobalScope.launch(handler) {
launch {
try {
delay(Long.MAX_VALUE) // it gets cancelled when another sibling fails with IOException
} finally {
throw ArithmeticException() // the second exception
}
}
launch {
delay(100)
throw IOException() // the first exception
}
delay(Long.MAX_VALUE)
}
job.join()
}
输出:
CoroutineExceptionHandler got java.io.IOException with suppressed [java.lang.ArithmeticException]
单独说一下async
async比较特殊:
- 作为top coroutine时, 在await的时候try-catch异常.
- 如果是非top coroutine, async块里的异常会被立即抛出.
例子:
fun main() {
val scope = CoroutineScope(SupervisorJob())
val deferred = scope.async {
throw RuntimeException("RuntimeException in async coroutine")
}
scope.launch {
try {
deferred.await()
} catch (e: Exception) {
println("Caught: $e")
}
}
Thread.sleep(100)
}
这里由于用了SupervisorJob, 所以async是top coroutine.
fun main() {
val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, exception ->
println("Handle $exception in CoroutineExceptionHandler")
}
val topLevelScope = CoroutineScope(SupervisorJob() + coroutineExceptionHandler)
topLevelScope.launch {
async {
throw RuntimeException("RuntimeException in async coroutine")
}
}
Thread.sleep(100)
}
当它不是top coroutine时, 异常会被直接抛出.
特殊的CancellationException
CancellationException是特殊的exception, 会被异常处理机制忽略, 即便抛出也不会向上传递, 所以不会取消它的parent.
但是CancellationException不能被catch, 如果它不被抛出, 其实协程没有被成功cancel, 还会继续执行.
CancellationException的透明特性:
如果CancellationException是由内部的其他异常引起的, 它会向上传递, 并且把原始的那个异常传递上去.
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
val job = GlobalScope.launch(handler) {
val inner = launch { // all this stack of coroutines will get cancelled
launch {
launch {
throw IOException() // the original exception
}
}
}
try {
inner.join()
} catch (e: CancellationException) {
println("Rethrowing CancellationException with original cause")
throw e // cancellation exception is rethrown, yet the original IOException gets to the handler
}
}
job.join()
}
输出:
Rethrowing CancellationException with original cause
CoroutineExceptionHandler got java.io.IOException
这里Handler拿到的是最原始的IOException.
Further Reading
官方文档:
Android官方文档上链接的博客和视频:
- Exceptions in coroutines
- KotlinConf 2019: Coroutines! Gotta catch 'em all! by Florina Muntenescu & Manuel Vivo
其他:
- Kotlin Coroutines and Flow - Use Cases on Android
- Why exception handling with Kotlin Coroutines is so hard and how to successfully master it!
[Kotlin Tutorials 22] 协程中的异常处理的更多相关文章
- Lua的函数调用和协程中,栈的变化情况
Lua的函数调用和协程中,栈的变化情况 1. lua_call / lua_pcall 对于这两个函数,对栈底是没有影响的--调用的时候,参数会被从栈中移除,当函数返 回的时候,其返回值会从函数处 ...
- Python协程中使用上下文
在Python 3.7中,asyncio 协程加入了对上下文的支持.使用上下文就可以在一些场景下隐式地传递变量,比如数据库连接session等,而不需要在所有方法调用显示地传递这些变量.使用得当的话, ...
- python 并发专题(十三):asyncio (二) 协程中的多任务
. 本文目录# 协程中的并发 协程中的嵌套 协程中的状态 gather与wait . 协程中的并发# 协程的并发,和线程一样.举个例子来说,就好像 一个人同时吃三个馒头,咬了第一个馒头一口,就得等这口 ...
- Swoole 中协程的使用注意事项及协程中的异常捕获
协程使用注意事项 协程内部禁止使用全局变量,以免发生数据错乱: 协程使用 use 关键字引入外部变量到当前作用域禁止使用引用,以免发生数据错乱: 不能使用类静态变量 Class::$array / 全 ...
- 关于python协程中aiorwlock 使用问题
最近工作中多个项目都开始用asyncio aiohttp aiomysql aioredis ,其实也是更好的用python的协程,但是使用的过程中也是遇到了很多问题,最近遇到的就是 关于aiorwl ...
- kotlin学习-Coroutines(协程)
协程(又名纤程),轻量级线程(建立在线程基础上,属于用户态调用),非阻塞式编程(像同步编写一样),在用户态内进行任务调度,避免与内核态过多交互问题,提高程序快速响应.协程使用挂起当前上下文替代阻塞,被 ...
- 结合Thread Ninja明确与处理异步协程中的异常
Thread Ninja说明: Thread Ninja - Multithread Coroutine Requires Unity 3.4.0 or higher. A simple script ...
- [Unity] 在协程中等待指定的毫秒
先写一个静态类: /// <summary> /// 公用基础函数库 /// <remarks>作者: YangYxd</remarks> /// </sum ...
- 使用context关闭协程以及协程中的协程
package main import ( "sync" "context" "fmt" "time" ) var wg ...
- Kotlin协程解析系列(上):协程调度与挂起
vivo 互联网客户端团队- Ruan Wen 本文是Kotlin协程解析系列文章的开篇,主要介绍Kotlin协程的创建.协程调度与协程挂起相关的内容 一.协程引入 Kotlin 中引入 Corout ...
随机推荐
- Condition 接口
系统性学习,移步IT-BLOG Java 对象拥有一组监视方法:wait().wait(long timeout).notify() 以及 notifyAll() 方法,这些方法与 synchroni ...
- 磁盘IO 基本常识
计算机硬件性能在过去十年间的发展普遍遵循摩尔定律,通用计算机的 CPU主频早已超过3GHz,内存也进入了普及DDR4的时代.然而传统硬盘虽然在存储容量上增长迅速,但是在读写性能上并无明显提升,同时SS ...
- Linux环境下使用jsoncpp
目录 1. 下载jsoncpp 2. 生成静态库libjsoncpp.a 3. 复制相关文件至/usr/local下(方便编程) 4. CMakeList.txt编写(需要新增的) 1. 下载json ...
- 这年头,谁的好友列表还没有躺一个ChatGPT啊?
你要是说这个,我可不困了 大家好,我最近开始使用一款非常有趣的AI机器人,它叫做ChatGPT.ChatGPT是一款独特的聊天机器人,它可以进行智能对话,回答你的问题,还可以学习你的语言习惯,使得对话 ...
- MATLAB计算变异函数并绘制经验半方差图
本文介绍基于MATLAB求取空间数据的变异函数,并绘制经验半方差图的方法. 由于本文所用的数据并不是我的,因此遗憾不能将数据一并展示给大家:但是依据本篇博客的思想与对代码的详细解释,大家用自己 ...
- AWS IAM介绍
前言 AWS是世界上最大的云服务提供商,它提供了很多组件供消费者使用,其中进行访问控制的组件叫做IAM(Identity and Access Management), 用来进行身份验证和对AWS资源 ...
- 二进制安装Kubernetes(k8s) v1.24.0 IPv4/IPv6双栈 (三主俩从)
二进制安装Kubernetes(k8s) v1.24.0 IPv4/IPv6双栈 (三主俩从) Kubernetes 开源不易,帮忙点个star,谢谢了 介绍 kubernetes二进制安装 后续尽可 ...
- [Linux]常用命令之【mount/umount】
1 mount mount命令的作用是加载文件系统,它的用权限是超级用户或/etc/fstab中允许的使用者. 在Linux和Unix系统上,所有文件都是作为一个大型树(以/为根)的一部分访问的. 要 ...
- [Linux]mysql错误总结-ERROR 1067 (42000): Invalid default value for TIMESTAMP
MySQL的TIMESTAMP类型的默认值设置无效. 0 使用环境描述 Linux CentOS 7.8.2003 x86/64bit MySQL: 5.7.24 (mysql --version / ...
- 10分钟带你徒手做个Java线程池
摘要:花10分钟开发一个极简版的Java线程池,让小伙伴们更好的理解线程池的核心原理. 本文分享自华为云社区<放大招了,冰河带你10分钟手撸Java线程池,yyds,赶快收藏吧>,作者:冰 ...