美文网首页
协程异常处理机制和协程作用域

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

作者: ModestStorm | 来源:发表于2022-11-02 12:38 被阅读0次

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

    相关文章

      网友评论

          本文标题:协程异常处理机制和协程作用域

          本文链接:https://www.haomeiwen.com/subject/qmdjtdtx.html