子协程的异常、取消导致整个作用域中协程的异常、取消的原因详见:[kotlin中CoroutineScope CoroutineContext的理解_王温暖的博客-CSDN博客](https://blog.csdn.net/cpcpcp123/article/details/113348214 中的协程作用域有如下三种:
通过 GlobeScope 启动的协程单独启动一个协程作用域,内部的子协程遵从默认的作用域规则。意味着这是一个独立的顶级协程作用域通过 GlobeScope 启动的协程“自成一派”。
coroutineScope 是继承外部 Job 的上下文创建作用域,在其内部的取消操作是双向传播的,子协程未捕获的异常也会向上传递给父协程。它更适合一系列对等的协程并发的完成一项工作,任何一个子协程异常退出,那么整体都将退出,简单来说就是”一损俱损“。这也是协程内部再启动子协程的默认作用域。
supervisorScope 同样继承外部作用域的上下文,但其内部的取消操作是单向传播的,父协程向子协程传播,反过来则不然,这意味着子协程出了异常并不会影响父协程以及其他兄弟协程。它更适合一些独立不相干的任务,任何一个任务出问题,并不会影响其他任务的工作,简单来说就是”自作自受“,例如 UI,我点击一个按钮出了异常,其实并不会影响手机状态栏的刷新。需要注意的是,supervisorScope 内部启动的子协程内部再启动子协程,如无明确指出,则遵守默认作用域规则,也即 supervisorScope 只作用域其直接子协程。
1、更安全地处理async{}中的异常
async构建器启动的协程中发生非CancellationException异常,会向外抛出,让其父协程及其他子协程停止。
如下,其中一个子协程(即 two)失败,并且它抛出了一个异常,第一个 async 以及等待中的父协程都会被取消, 所有在作用域中启动的协程都会被取消。
import kotlinx.coroutines.*
fun main() = runBlocking<Unit> {
try {
failedConcurrentSum()
} catch(e: ArithmeticException) {
println("Computation failed with ArithmeticException")
}
}
suspend fun failedConcurrentSum(): Int = coroutineScope {
val one = async<Int> {
try {
delay(Long.MAX_VALUE) // 模拟一个长时间的运算
42
} finally {
println("First child was cancelled")
}
}
val two = async<Int> {
println("Second child throws an exception")
throw ArithmeticException()
}
one.await() + two.await()
}
请注意,如果其中一个子协程(即 two)失败,第一个 async 以及等待中的父协程都会被取消
Second child throws an exception
First child was cancelled
Computation failed with ArithmeticException
为了解决上述问题,可以使用SupervisorJob替代Job,SupervisorJob与Job基本类似,区别在于不会被子协程的异常所影响。
import kotlinx.coroutines.*
val job: Job = SupervisorJob()
val scope = CoroutineScope(Dispatchers.Default + job)
suspend fun doWork1(): Deferred<Int> = scope.async {
delay(3000)
111
}
suspend fun doWork2(): Deferred<Int> = scope.async {
delay(1000)
throw ArithmeticException()
121
}
fun main() {
runBlocking {
var work1 = 0
var work2 = 0
work1 = doWork1().await()
println("work1 result $work1")
try {
work2 = doWork2().await()
println("work2 result $work2")
} catch (e: Exception) {
println("dowork2 catch $e")
}
println("final: ${work1 + work2}")
}
}
对Job进行cancel操作
如果想取消当前启动的所有子协程,同时不影响后续的新协程的启动,应该使用CoroutineContext.cancelChildren()
对Job进行cancel,Job关联的所有子协程都将停止的同时,Job变为Completed状态,此后无法再用此Job启动协程
import kotlinx.coroutines.*
class WorkManager {
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.Default + job)
fun doWork1() {
scope.launch {
println("doWork1")
}
}
fun doWork2() {
scope.launch {
println("doWork2")
}
}
fun cancelAllWork() {
// job.cancel()//以后再起的job无法工作
scope.coroutineContext.cancelChildren()//以后再起来的job可以工作
}
}
fun main() {
val workManager = WorkManager()
workManager.doWork1() // (1)
workManager.doWork2() // (2)
workManager.cancelAllWork()
workManager.doWork1() // (3)
}
如上,如果使用cancel(),最后的dowork1没有打印:
doWork1
doWork2
如果使用cancelChildren(),cancel后最后的dowork1也打印了:
doWork1
doWork2
doWork1
注意GlobalScope的使用场景
在Android中不要随处使用GlobalScope,GlobalScope应该仅用于Application级别的任务,且生命周期应该与App一致,不应该在中途被Cancel.
网友评论