美文网首页Kotlin-Coroutines
协程中的取消和异常 (取消操作详解)

协程中的取消和异常 (取消操作详解)

作者: 大虾啊啊啊 | 来源:发表于2020-12-03 09:54 被阅读0次

    在开发中,我们要避免不必要的的任务来节约设备的内存和电量的使用,协程也是如此。在使用的过程我们需要控制好它的生命周期,在不需要它的取消它。

    调用cancel方法

    取消作用域会取消它的子协程

    当启动了很多个协程,我们一个个协程的取消比较麻烦,我们可以通过取消整个作用域来解决这个问题,因为取消作用域可以取消该作用域创建的所有协程。

    / 假设我们已经定义了一个作用域
    
    val job1 = scope.launch { … }
    val job2 = scope.launch { … }
    
    scope.cancel()
    

    假设我们创建了一个作用域scope,并创建了两个协程job1和job2。我们通过调用scope.cancel(),取消作用域,将会把job1 和job2两个协程都取消。

    单独取消某个协程,不会影响他的兄弟协程

    我们创建了两个协程,job1和job2.我们单独取消job1,不会影响到job2

    
    // 假设我们已经定义了一个作用域
    
    val job1 = scope.launch { … }
    val job2 = scope.launch { … }
     
    // 第一个协程将会被取消,而另一个则不受任何影响
    job1.cancel()
    

    协程通过抛出一个特殊的异常 CancellationException 来处理取消操作

    在调用cancel函数的时候,我们需要传入一个CancellationException对象,如果我们没有传入,那就用默认的defaultCancellationException。

     // external cancel with cause, never invoked implicitly from internal machinery
        public override fun cancel(cause: CancellationException?) {
            cancelInternal(cause ?: defaultCancellationException())
        }
    

    一旦抛出了CancellationException,我们就可以通过这一机制来处理协程的取消。在底层的实现中,子协程会通过抛出异常的方式将取消的情况通知它的父级,父协程通过传入的取消原因决定是否处理该异常。

    不能在已取消的作用域中再次启动新的协程

    调用了 cancel 方法为什么协程处理的任务没有停止?

    不同的Diapatcher不同的区别,下一篇文章将介绍。
    我们以Dispatchers.Default为例子

    import kotlinx.coroutines.*
    
    suspend fun main() = runBlocking {
        var startTime = System.currentTimeMillis()
        val job = launch(Dispatchers.Default) {
            var nextTime = startTime
            var i = 0
            while (i < 5) {
                if (System.currentTimeMillis() >= nextTime) {
                    println("这是第${i}次")
                    i++
                    //1000毫秒执行一次
                    nextTime += 1000
                }
            }
        }
        delay(1000)
        println("取消")
        job.cancel()
        println("取消完毕")
    
    }
    
    
    这是第0次
    这是第1次
    取消
    取消完毕
    这是第2次
    这是第3次
    这是第4次
    

    调用cancel方法之后,协程的任务依然在运行。调用cancel方法的时候,此时协程处于cancelling正在取消的状态,接着我们打印了2,3,4,处理任务结束之后,协程变成cancelled已经取消的状态,这是以Default举例,Default调度会等待协程任务处理完毕才取消。

    让协程可以被取消

    协程处理任务都是协作式的,协作的意思就是我们的处理任务要配合协程取消做处理。因此在执行任务期间我们要定时检查协程的状态是否已经取消,例如我们从磁盘读取文件之前我们先检查协程是否被取消了。

    
    val job = launch {
        for(file in files) {
            // TODO 检查协程是否被取消
            readFile(file)
        }
    }
    

    协程中的挂起函数都是可取消的,使用他们的时候,我们不需要检查协程是否已取消。例如withContext,delay 。如果没有这些挂起函数,为了让我们的代码配合协程取消,可以使用一下两种方法:

    • 检查 job.isActive 或者使用 ensureActive()
    • 使用 yield() 来让其他任务进行

    检查 job 的活跃状态

    先看一下第一种方法,在我们的 while(i<5) 循环中添加对于协程状态的检查:

    // 因为处于 launch 的代码块中,可以访问到 job.isActive 属性
    while (i < 5 && isActive)
    

    使用 yield() 函数运行其他任务

    Job.join 和 Deferred.await cancellation

    等待协程处理结果有两种方法,launch启动的job可以调用join,async 返回的Deferred 可以调用await方法

    • job.join会让协程挂起,直到等待协程处理任务完毕,我们可以配合cancel使用
    • deferred.await()如果我们关心协程的处理结果,我们可以使用deferred。结果由deferred.await返回。也是job类型,也可以被取消。

    处理协程取消的副作用

    当我们需要在协程取消 后处理一些清理的工作,或者做一些打印日志。我们有几种办法:

    • 通过检查协程的状态
    
    while (i < 5 && isActive) {
        if (…) {
            println(“Hello ${i++}”)
            nextPrintTime += 500L
        }
    }
     
    // 协程所处理的任务已经完成,因此我们可以做一些清理工作
    println(“Clean up!”
    

    当判断协程不是isActive状态的时候,我们可以做一些清理

    • try catch finally
      我们知道协程的取消会抛出CancellationException 异常,我们可以在协程提中使用try catch finally,在finally中做我们的一些清理的工作,或者打印日志
    
    val job = launch {
       try {
          work()
       } catch (e: CancellationException){
          println(“Work cancelled!”)
        } finally {
          println(“Clean up!”)
        }
    }
    
    delay(1000L)
    println(“Cancel!”)
    job.cancel()
    println(“Done!
    

    已经取消的协程,不能再被挂起

    已经取消的协程,不能再被挂起,但是当我们需要在取消的协程中调用挂起函数,那么我们可以在finally中使用NonCancellable ,意思是让协程挂起,直到处理挂起函数中的代码完毕,协程才会取消。

    
    val job = launch {
       try {
          work()
       } catch (e: CancellationException){
          println(“Work cancelled!”)
        } finally {
          withContext(NonCancellable){
             delay(1000L) // 或一些其他的挂起函数
             println(“Cleanup done!”)
          }
        }
    }
    
    delay(1000L)
    println(“Cancel!”)
    job.cancel()
    println(“Done!
    

    在jetpack中使用viewModelScope 或者lifecycleScope 中定义的作用域,他们在scope完成后取消他们的处理任务。如果我们手动创建自己的作用域CoroutineScope,我们需要协作协程,将我们的作用域和job绑定,在需要的时候取消。

    相关文章

      网友评论

        本文标题:协程中的取消和异常 (取消操作详解)

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