目录

取消与超时

这一部分包含了协程的取消与超时。

取消协程的执行

在长时间运行的应用程序中,你可能需要对后台协程进行细粒度控制。 例如,用户可能已关闭启动协程的页面,现在不再需要其结果,并且该操作是可取消的。 launch函数返回一个可用于取消运行协同程序的job

  1. import kotlinx.coroutines.*
  2. fun main() = runBlocking {
  3. val job = launch {
  4. repeat(1000) { i ->
  5. println("I'm sleeping $i ...")
  6. delay(500L)
  7. }
  8. }
  9. delay(1300L) // delay a bit
  10. println("main: I'm tired of waiting!")
  11. job.cancel() // cancels the job
  12. job.join() // waits for job's completion
  13. println("main: Now I can quit.")
  14. }

运行结果如下:

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

job.cancel 调用后,其它协程将不能从它获取任何结果,因为该协程已经取消.还有一个Job扩展函数cancelAndJoin,它结合cancel和join调用。

取消是协作的

协程的取消是协作的,协程代码必须合作才能取消. kotlinx.coroutines中所有的挂起函数都是可取消的.它们会检查协程是否可取消,若不可取消则抛出 [CancellationException](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-cancellation-exception/index.html) 异常. 然而如果协程正在进行计算并且没有检查可取消性, 那么它是不可取消的,比如下面的例子:

  1. import kotlinx.coroutines.*
  2. fun main() = runBlocking {
  3. val startTime = System.currentTimeMillis()
  4. val job = launch(Dispatchers.Default) {
  5. var nextPrintTime = startTime
  6. var i = 0
  7. while (i < 5) { // computation loop, just wastes CPU
  8. // print a message twice a second
  9. if (System.currentTimeMillis() >= nextPrintTime) {
  10. println("I'm sleeping ${i++} ...")
  11. nextPrintTime += 500L
  12. }
  13. }
  14. }
  15. delay(1300L) // delay a bit
  16. println("main: I'm tired of waiting!")
  17. job.cancelAndJoin() // cancels the job and waits for its completion
  18. println("main: Now I can quit.")
  19. }

可以试试它是否会在取消后继续打印“I’m sleeping”,直到作业在五次迭代后自行完成。

使计算代码可取消

有两种方法可以使计算代码可以取消。 一种是定期调用检查取消的挂起功能。 yield函数是一个很好的选择。 另一种是明确检查取消状态。 本例使用后一种方法:

while (i < 5) 改为 while (isActive) 并运行

  1. import kotlinx.coroutines.*
  2. fun main() = runBlocking {
  3. val startTime = System.currentTimeMillis()
  4. val job = launch(Dispatchers.Default) {
  5. var nextPrintTime = startTime
  6. var i = 0
  7. while (isActive) { // cancellable computation loop
  8. // print a message twice a second
  9. if (System.currentTimeMillis() >= nextPrintTime) {
  10. println("I'm sleeping ${i++} ...")
  11. nextPrintTime += 500L
  12. }
  13. }
  14. }
  15. delay(1300L) // delay a bit
  16. println("main: I'm tired of waiting!")
  17. job.cancelAndJoin() // cancels the job and waits for its completion
  18. println("main: Now I can quit.")
  19. }

正如你所看到的,现在循环可以取消. [isActive](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/is-active.html) 是一个扩展属性,可通过CoroutineScope对象在coroutine代码中使用。

在 finally 中释放资源

可取消的挂起函数会在取消时抛出CancellationException,这可以通过常规方式处理。 例如,try{…} finally {…} 表达式或者Kotlin use 函数在取消协程时正常执行其终止操作:

  1. val job = launch {
  2. try {
  3. repeat(1000) { i ->
  4. println("I'm sleeping $i ...")
  5. delay(500L)
  6. }
  7. } finally {
  8. println("I'm running finally")
  9. }
  10. }
  11. delay(1300L) // delay a bit
  12. println("main: I'm tired of waiting!")
  13. job.cancelAndJoin() // cancels the job and waits for its completion
  14. println("main: Now I can quit.")

join和cancelAndJoin都等待所有终结操作完成,因此上面的示例生成以下输出:

  1. I'm sleeping 0 ...
  2. I'm sleeping 1 ...
  3. I'm sleeping 2 ...
  4. main: I'm tired of waiting!
  5. I'm running finally
  6. main: Now I can quit.

运行不可取消的代码块

在前一个示例的finally块中使用挂起函数的任何尝试都会导致CancellationException,因为取消了运行此代码的协程。 通常,这没关系,因为所有表现良好的关闭操作(关闭文件,取消作业或关闭任何类型的通信通道)通常都是非阻塞的,并且不涉及任何挂起函数。 但是,在极少数情况下,当您需要在取消的协同程序中挂起时,可以使用withContext函数和NonCancellable上下文将相应的代码包装在withContext(NonCancellable){…}中,如下例所示:

  1. val job = launch {
  2. try {
  3. repeat(1000) { i ->
  4. println("I'm sleeping $i ...")
  5. delay(500L)
  6. }
  7. } finally {
  8. withContext(NonCancellable) {
  9. println("I'm running finally")
  10. delay(1000L)
  11. println("And I've just delayed for 1 sec because I'm non-cancellable")
  12. }
  13. }
  14. }
  15. delay(1300L) // delay a bit
  16. println("main: I'm tired of waiting!")
  17. job.cancelAndJoin() // cancels the job and waits for its completion
  18. println("main: Now I can quit.")

超时

在实践中取消协程执行的最主要的原因是因为它的执行时间超过了限制。 虽然可以手动跟踪对相应作业的引用并启动单独的协程以在延迟后取消跟踪的协程,但withTimeout函数是一个开箱即用的操作。 请看以下示例:

  1. withTimeout(1300L) {
  2. repeat(1000) { i ->
  3. println("I'm sleeping $i ...")
  4. delay(500L)
  5. }
  6. }

输出结果如下:

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

withTimeout抛出的TimeoutCancellationException是CancellationException的子类。 我们之前没有看到它的堆栈跟踪打印在控制台上。 这是因为在取消的协程中,CancellationException被认为是协程完成的正常原因。 但是,在这个例子中,我们在main函数中使用了withTimeout。

因为取消只是一个异常,所有资源都以通常的方式关闭。 如果你可以在任何类型的超时上做一些额外的操作或者使用类似于withTimeout的withTimeoutOrNull函数,你可以在try {…} catch(e:TimeoutCancellationException){…}块中用超时包装代码, 这样在超时时将返回null而不是抛出异常:

  1. val result = withTimeoutOrNull(1300L) {
  2. repeat(1000) { i ->
  3. println("I'm sleeping $i ...")
  4. delay(500L)
  5. }
  6. "Done" // will get cancelled before it produces this result
  7. }
  8. println("Result is $result")

这样运行以上代码就不会抛出异常了:

  1. I'm sleeping 0 ...
  2. I'm sleeping 1 ...
  3. I'm sleeping 2 ...
  4. Result is null