协程SupervisorJob使用

子协程的异常、取消导致整个作用域中协程的异常、取消的原因详见:kotlin中CoroutineScope CoroutineContext的理解_王温暖的博客-CSDN博客

kotlin中的协程作用域有如下三种:

通过 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 = Job()
val scope = CoroutineScope(Dispatchers.Default + job)
 
suspend fun doWork1(): Deferred<Int> = scope.async {
    delay(10)
    111
}
 
suspend fun doWork2(): Deferred<Int> = scope.async {
    delay(3000)
    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}")
    }
}

打印如下:

work1 result 111
dowork2 catch java.lang.ArithmeticException
final: 111


2、对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


3、注意GlobalScope的使用场景
在Android中不要随处使用GlobalScope,GlobalScope应该仅用于Application级别的任务,且生命周期应该与App一致,不应该在中途被Cancel

————————————————
版权声明:本文为CSDN博主「沙漠一只雕得儿得儿」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/cpcpcp123/article/details/113343549

### 如何正确地取消 Kotlin 协程 #### 创建可取消的协程Kotlin 中,创建一个可以被取消的协程通常涉及使用 `launch` 或者 `async` 函数启动一个新的协程。为了确保能够正常取消协程,在定义协程时应当指定其所属的作用域[^4]。 ```kotlin val scope = CoroutineScope(Job()) scope.launch { // 执行异步任务... } ``` #### 使用 `SupervisorJob` 和 `CoroutineScope` 当多个子协程共享同一个父级作用域时,如果其中一个子协程失败,则整个作用域中的其他所有活动协程都将被取消。为了避免这种情况发生,可以考虑使用 `SupervisorJob` 来代替默认的 `Job()`,这样即使某个子协程抛出了未捕获异常也不会影响到其他的兄弟协程。 ```kotlin val supervisor = SupervisorJob() val parentScope = CoroutineScope(supervisor) // 子协程1 parentScope.launch { delay(1000L) println("Child 1 completed.") } // 子协程2 parentScope.launch { try { delay(Long.MAX_VALUE) // 模拟长时间运行的任务 } catch (e: CancellationException) { println("Child 2 was cancelled.") } } delay(500L) supervisor.cancel() // 取消父作用域下的所有子协程 println("All children have been cancelled.") ``` #### 定期检查取消状态 为了让协程能够在接收到取消请求后及时响应并停止执行当前的工作流,可以在协程内部周期性地调用 `isActive` 属性来进行自我检测;也可以利用诸如 `yield()`、`withTimeoutOrNull()` 这样的内置挂起函数来实现相同的效果[^3]。 ```kotlin coroutineScope { launch { while(isActive){ doSomeWork() yield() // 让出CPU时间片给其他协程,并且会检查是否已经被取消 } } } ``` #### 注册取消回调 对于那些可能持有外部资源(如网络连接或文件句柄)的情况来说,在协程结束之前完成必要的清理工作是非常重要的。为此,可以通过注册 `invokeOnCancellation` 回调方法来确保无论因为什么原因导致协程终止都能触发相应的收尾逻辑[^5]。 ```kotlin suspend fun performTaskWithResource(): Unit = withContext(NonCancellable) { // 防止此部分代码因上级取消而中断 suspendCancellableCoroutine<Unit> { cont -> val resource = acquireResource() cont.invokeOnCancellation { releaseResource(resource) } try { useResource(resource) cont.resume(Unit) } finally { if (!cont.isCompleted) { releaseResource(resource) } } } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值