给出下面代码:
lifecycleScope.launch(Dispatchers.IO) {
val task1 = async {
throw RuntimeException("task1 failed")
}
val task2 = async {
throw RuntimeException("task2 failed")
}
try {
task1.await()
} catch (e: Throwable){
Log.i("test", "catch task1: $e")
}
Log.i("test", "is coroutine active: $isActive")
try {
task2.await()
} catch (e: Throwable){
Log.i("test", "catch task2: $e")
}
Log.i("test", "scope end.")
}
问:app 会产生什么?输出的日志是怎姿态的?为什么?
……
……
……
……
……
……
……
……
……
答:app 会 crash,输出日志为
I/test: catch task1: java.lang.RuntimeException: task1 failed
I/test: is coroutine active: false
I/test: catch task2: kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=DeferredCoroutine{Cancelling}
I/test: scope end.
魔幻吗?
那咱们就来剖析下为啥结果是这个姿态的。
协程有一个很根底的设定:默许情况下,反常会往外层 scope 抛,用以马上撤销外层 scope 内的其它的子 job。
在上面的比如中,假设:lifecycleScope.launch 创立的子 scope 为 A。task1 用 async 创立 scope A 的子 scope 为 B。task2 用 async 创立 scope A 的子 scope 为 C。
当 scope B 产生反常,scope B 会将反常抛给 scope A,scope A 会 cancel 掉 task2 和自己,再把反常抛给 lifecycleScope,由于 lifecycleScope 没有 CoroutineExceptionHandler 并且 scope A 是通过 launch 发动的,所以 crash 就产生了。
那怎么打断反常的这个传达链呢?
答案便是使用 SupervisorJob,或许用根据它的 supervisorScope。它不会把反常往上抛,也不会撤销掉其它的子 job。但是,SupervisorJob 对 launch 和 async 发动的协程的态度是不一样的,它的源码注释里写明晰的,简略的认为它会吃掉反常是会踩坑的。
翻译出来便是,假如是 launch 发动的子协程,是需求 CoroutineExceptionHandler 配合处理的,假如是 async 发动的协程,便是真的不抛,比及 Deferred.await 时再抛。
所以,在上面的代码中,虽然 lifecycleScope 有用到 SupervisorJob,但反常从 scopeA 往上传时,由于没有 CoroutineExceptionHandler,所以跪了。
那么为什么 async 要往上抛反常,导致 await 的 try catch 还需求 supervisorScope 的配合?感觉有点反人类?
想象一下下面的场合:
lifecycleScope.launch {
val task1 = async { "十分耗时的操作,但没有反常" }
val task2 = async { throw RuntimeException("") }
val result1 = task1.await()
val result2 = task2.await()
}
由于 task2 有反常,所以整个协程必定会失利。假如等 await 时才跑错误, 那么就需求等耗时的 task1 履行完成,轮到 task 的 await 调用时,反常才能跑出来,虽然也没啥问题,便是白白耗费了 task1 的履行。
而依据当前的设计,task2 抛出反常,那么外层 scope 就会把 task1 也给撤销了,整个 scope 也就履行完毕了。async 源码里说到的原因是为了 structured concurrency,也是希望使用者更多的重视 scope 以及 scope 内各个任务的关联联系吧。不过这坑的确有点让人有时摸不着头脑,或许今后就变了也说不定。
剩下一个问题是,task1 失利后就往上抛吗?为啥 catch task1 后还有日志打印出来?
其实上面现已说到了,反常抛给 scope A 后,它会 cancel 掉自己,再往上抛,而 cancel 掉自己并不是强制终止掉协程的履行,而是先改变状况为 cancelling,所以日志中 isActive 现已变成 false 了,第二个反常也不是 task2 的反常,而是 await 本身抛出的 CancellationException。这儿告诉咱们要注意两点:
- try catch 时假如是 CancellationException,要记得 rethrow。
- 一些循环、耗时的点,要记得用 isActive 或许 ensureActive 查看,不要写出不能正常 cancel 的协程。像 delay 等 api,官方现已做好了这方面的查看,极大地方便了开发者。这个线程 interrupted 相关知识点是同一个道理。
了解了各种坑点以及背后的原因,咱们就可以把协程用得飞起了。最终,修复文章最初说到的问题,便是简略包个 supervisorScope 就行啦。
lifecycleScope.launch(Dispatchers.IO) {
supervisorScope {
val task1 = async {
throw RuntimeException("task1 failed")
}
val task2 = async {
throw RuntimeException("task2 failed")
}
try {
task1.await()
} catch (e: Throwable){
Log.i("test", "catch task1: $e")
}
Log.i("test", "is coroutine active: $isActive")
try {
task2.await()
} catch (e: Throwable){
Log.i("test", "catch task2: $e")
}
Log.i("test", "scope end.")
}
}
最终,引荐下我的大众号,快来重视下: