Kotlin协程的取消与超时(五)

一、前言

当使用协程进行异步任务的时候,往往也会因为一些情况对其进行取消。取消异步任务通常使用Job.cancel()函数

二、cancel()

对于cancel()的使用方式如下:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        repeat(1000) { i ->
            println("job: I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancel() // cancels the job
    job.join() // waits for job's completion 
    println("main: Now I can quit.")    
}

那么运行结果如下:

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.

由此可以看出来在协程取消后程序也就自动停止了。
另外当协程取消后,子协程也会被递归取消

三、检查协程的运行状态

协程理应是可以取消的,但如果我们在协程里面做一些复杂的操作并且不进行检查,那么即使协程进行了取消操作,但是里面的任务依然会执行完毕。

import kotlinx.coroutines.*

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) { // computation loop, just wastes CPU
            // print a message twice a second
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancels the job and waits for its completion
    println("main: Now I can quit.")    
}

执行结果如下

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm sleeping 3 ...
job: I'm sleeping 4 ...
main: Now I can quit.

可以看出已经取消的任务,最后依然执行了剩下的操作。所以这里面有两种解决方案,一种是使用yield()函数,另外一种就是检查好自身多协程状态。这里使用第二种方式

import kotlinx.coroutines.*

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (isActive) { // cancellable computation loop
            // print a message twice a second
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancels the job and waits for its completion
    println("main: Now I can quit.")    
}

结果如下

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.

可以看出线程取消后,后面逻辑就不再执行了
拓展:
关于取消的操作详细可以查看
kotlin 协程之取消协程

关于yield()的方式可以使用以下方式:

val b = runBlocking<Unit> {
        val startTime = System.currentTimeMillis()
        val job = launch(Dispatchers.Default) {
            var nextPrintTime = startTime
            var i = 0
            while (i < 5 ) {
                 yield()//会抛出CancellationException
                if (System.currentTimeMillis() >= nextPrintTime) {
                    println("job:I'm sleeping ${i++} .....")
                    nextPrintTime += 500
                }

            }
        }
        delay(1300)
        println("main:I'm tired of waiting!")
        job.cancelAndJoin()
        println("main:Now I can quit.")
    }

使用ensureActive()来判断操作

 val b = runBlocking<Unit> {
        val startTime = System.currentTimeMillis()
        val job = launch(Dispatchers.Default) {
            var nextPrintTime = startTime
            var i = 0
            while (i < 5 ) {
                ensureActive()//会抛出CancellationException
                if (System.currentTimeMillis() >= nextPrintTime) {
                    println("job:I'm sleeping ${i++} .....")
                    nextPrintTime += 500
                }

            }
        }
        delay(1300)
        println("main:I'm tired of waiting!")
        job.cancelAndJoin()
        println("main:Now I can quit.")
    }

四、finally

可取消的挂起函数会在取消时抛出CancellationException(但是这并不会有堆栈信息,因为系统认为取消四结束任务的标志,不需要暴漏出来),这可以用通常的方式处理。例如,当一个协程被取消时,try {...} finally {...}表达式和 Kotlinuse函数会正常执行它们的终结动作:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                println("job: I'm sleeping $i ...")
                delay(500L)
            }
        } finally {
            println("job: I'm running finally")
        }
    }
    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancels the job and waits for its completion
    println("main: Now I can quit.")    
}

结果如下:

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm running finally
main: Now I can quit.

以下参考链接kotlin 协程之取消协程
use该函数只能被事项了Closeable的对象使用,程序结束的时候会自动调用close方法,适合文件对象

 val b = runBlocking<Unit> {
        BufferedReader(FileReader("file:///android_asset/citys.json")).use{
            var line: String?
            while (true) {
                line = readLine() ?: break
                println(line)
            }
        }
    }

五、 withContext(NonCancellable)

有时候需要在协程的finally中处理逻辑,但是在这里使用协程执行会出现错误。因为错误不会暴漏出来所以明面上只是不执行而已。比如以下代码

   @Test
    fun coroutinesCancelFinally(){
        runBlocking {
            val job = launch {
                try {
                    repeat(1000) { i ->
                        println("job: I'm sleeping $i ...")
                        delay(500L)
                    }
                } finally {
                    launch {
                        println("job: I'm running finally")
                        delay(1000L)
                        println("job: And I've just delayed for 1 sec because I'm non-cancellable")
                    }
                }
            }
            delay(1300L) // delay a bit
            println("main: I'm tired of waiting!")
            job.cancelAndJoin() // cancels the job and waits for its completion
//            job.cancel()
            println("main: Now I can quit.")
        }

执行后可以看到如下结果

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.

期待的finally块中的代码并没有执行,这里需要使用withContext(NonCancellable)来处理。所以修改为以下就可以了。需要注意的是该函数只能在这里使用

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                println("job: I'm sleeping $i ...")
                delay(500L)
            }
        } finally {
            withContext(NonCancellable) {
                println("job: I'm running finally")
                delay(1000L)
                println("job: And I've just delayed for 1 sec because I'm non-cancellable")
            }
        }
    }
    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancels the job and waits for its completion
    println("main: Now I can quit.")    
}

其结果如下

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm running finally
job: And I've just delayed for 1 sec because I'm non-cancellable
main: Now I can quit.

六、超时TimeOut

如果我们执行某个协程,当它超过了某个我们期待的时间后,可以主动将其取消,这里需要使用withTimeout()函数。

fun main() = runBlocking {
    withTimeout(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
}

其结果如下

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms

这里可以看到一个异常信息,这是因为超时被认为是一个需要用户知道的问题,如果不需要知道,可以使用withTimeoutOrNull来替代

    @Test
    fun coroutinesTimeOut(){
        runBlocking {
            val result = withTimeoutOrNull(1300L) {
                repeat(1000) { i ->
                    println("I'm sleeping $i ...")
                    delay(500L)
                }
                "Done" //假如任务没有超时可以返回一个结果,如果超时了会返回null,如果使用withTimeout则会抛出异常
            }
            println("Result is $result")
        }
    }

var acquired = 0

class Resource {
    init { acquired++ } // Acquire the resource
    fun close() { acquired-- } // Release the resource
}
  • 这里需要注意的是withTimeout 中的超时事件与其块中运行的代码是异步的,并且可能随时发生,甚至在从超时块内部返回之前。如果您在块内打开或获取某些需要在块外关闭或释放的资源。

    这里以一个例子作为解释(这里的例子其实并没有出现预想的问题,所以本能问题只作为记录)

@Test
  fun coroutinesTimeOut2(){
      runBlocking {
          repeat(200_000) { // Launch 100K coroutines
              launch(Dispatchers.IO) {
                  val resource = withTimeout(60) { // Timeout of 60 ms
                      delay(50) // Delay for 50 ms
                      Resource() // Acquire a resource and return it from withTimeout block
                  }
                  resource.close() // Release the resource
              }
          }
      }
      // Outside of runBlocking all coroutines have completed
      println(acquired) // Print the number of resources still acquired
  }

可以看到我们期望最终打印的值为0,但是多次运行后会发现偶尔打印出非0的值,可以使用以下方式修改

 @Test
      fun coroutinesTimeOut3(){
          runBlocking {
              repeat(100_000) { // Launch 100K coroutines
                  launch() {
                      var resource: Resource? = null // Not acquired yet
                      try {
                          withTimeout(60) { // Timeout of 60 ms
                              delay(50) // Delay for 50 ms
                              resource = Resource() // Store a resource to the variable if acquired
                          }
                          // We can do something else with the resource here
                      } finally {
                          resource?.close() // Release the resource if it was acquired
                      }
                  }
              }
          }
  // Outside of runBlocking all coroutines have completed
          println(acquired) // Print the number of resources still acquired
      }
  
  var acquired = 0
  
  class Resource {
      init { acquired++ } // Acquire the resource
      fun close() { acquired-- } // Release the resource
  }

七、参考链接

  1. 协程的取消和超时
    https://kotlinlang.org/docs/cancellation-and-timeouts.html
  2. Kotlin之协程(二)取消
  3. kotlin 协程之取消协程
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值