协程异常处理机制和协程作用域

2022-11-02  本文已影响0人  ModestStorm

子协程的异常、取消导致整个作用域中协程的异常、取消的原因详见:[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.

上一篇下一篇

猜你喜欢

热点阅读