跳转至

Kotlin协程取消与超时

更新日期 2021-9-27
  • 2021-9-27 创建文档
开发环境
  • IntelliJ IDEA 2021.2.2 (Community Edition)
  • Kotlin: 212-1.5.10-release-IJ5284.40

Kotlin协程系列目录

  1. Kotlin协程入门 —— 第一个例子,启动协程
  2. Kotlin协程基础 —— 阻塞与非阻塞,等待,作用域构建器
  3. Kotlin协程取消与超时 —— 取消的条件,超时设定


这里介绍协程的超时和取消。

取消

我们可以启动协程,也可以在协程尚未结束时,主动取消协程。

例如在Android应用中,一个界面的ViewModel启动了协程,而这个界面要关闭退出了。那么我们需要把协程也取消掉。

launch函数返回的Job即是协程对象。调用job.cancel()函数即可取消协程。

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = GlobalScope.launch {
        repeat(100) { i ->
            println("休眠次数 $i")
            delay(500)
        }
    }
    val t1 = System.currentTimeMillis()
    delay(1200)
    println("[rustfisher] 等待完毕准备退出 t1:$t1")
    job.cancel()
    job.join()
    println("Bye~ 耗时: ${System.currentTimeMillis() - t1}毫秒")
}

运行结果

休眠次数 0
休眠次数 1
休眠次数 2
[rustfisher] 等待完毕准备退出 t1:1632750920587
Bye~ 耗时: 1218毫秒

调用job.cancel(),通过log可以看出在它没有输出了,因为它被取消了。

我们也可以调用JobcancelAndJoin()方法来代替上面代码中的cancel()join()

// Job.kt 部分源码
public suspend fun Job.cancelAndJoin() {
    cancel()
    return join()
}

检查取消情况

协程在协作的情况下才能被取消。kotlinx.coroutines中的挂起函数都是可被取消的。

无法取消

如果协程在执行计算任务,并且没检查取消的话,那我们的取消尝试会失败。比如下面的代码

import kotlinx.coroutines.*

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0 // 模拟的控制循环数量
        while (i < 5) { // 模拟耗时计算
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("[job] 模拟耗时计算中 ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(800) // 等待一会
    println("[rustfisher] 尝试取消协程")
    job.cancelAndJoin()
    println("程序退出 bye~")
}
运行log
[job] 模拟耗时计算中 0 ...
[job] 模拟耗时计算中 1 ...
[rustfisher] 尝试取消协程
[job] 模拟耗时计算中 2 ...
[job] 模拟耗时计算中 3 ...
[job] 模拟耗时计算中 4 ...
程序退出 bye~

可以看到,模拟耗时计算直到4,整个程序退出。而调用cancelAndJoin()并没有成功取消掉协程。

可以取消

让协程可被取消的方法

  • 定期调用挂起函数来检查取消,例如yield
  • 显式的检查取消状态,例如检查isActive变量

对上面的代码进行一些改进。把while (i < 5)循环中的条件改成while (isActive)。修改后的代码如下

import kotlinx.coroutines.*

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (isActive) { // 模拟耗时计算
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("[job] 模拟耗时计算中 ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(800) // 等待一会
    println("[rustfisher] 尝试取消协程")
    job.cancelAndJoin()
    println("程序退出 bye~")
}
运行结果
[job] 模拟耗时计算中 0 ...
[job] 模拟耗时计算中 1 ...
[rustfisher] 尝试取消协程
程序退出 bye~
从log中看到,尝试取消协程后,协程不再进行计算。

在finally块中释放资源

取消协程时,挂起函数能抛出CancellationException。我们可以使用try-catch-finally来处理。并且在finally块中释放资源。

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                println("[job]模拟计算次数 $i ...")
                delay(300L)
            }
        } catch (e: CancellationException) {
            println("[job] CancellationException ${e.message}")
        } finally {
            println("[job][finally] 释放资源..")
        }
    }
    delay(800) // 等待一会
    println("[rustfisher] 尝试取消协程")
    job.cancelAndJoin()
    println("[rustfisher] 程序退出 bye~")
}
运行log
[job]模拟计算次数 0 ...
[job]模拟计算次数 1 ...
[job]模拟计算次数 2 ...
[rustfisher] 尝试取消协程
[job] CancellationException StandaloneCoroutine was cancelled
[job][finally] 释放资源..
[rustfisher] 程序退出 bye~

运行不能取消的代码块

让协程取消后,我们可能要执行关闭文件等操作,这些操作需要执行。可以把这些操作包在withContext(NonCancellable) { }中。

val job = launch {
    try {
        repeat(1000) { i ->
            println("[job]模拟计算次数 $i ...")
            delay(300L)
        }
    } catch (e: CancellationException) {
        println("[job] CancellationException ${e.message}")
    } finally {
        withContext(NonCancellable) {
            println("[job][finally] 进入NonCancellable")
            delay(1000) // 假设这里还有一些耗时操作
            println("[job][finally] NonCancellable完毕")
        }
        println("[job][finally] 结束")
    }
}
delay(800) // 等待一会
println("[rustfisher] 尝试取消协程")
job.cancelAndJoin()
println("[rustfisher] 程序退出 bye~")

运行log

[job]模拟计算次数 0 ...
[job]模拟计算次数 1 ...
[job]模拟计算次数 2 ...
[rustfisher] 尝试取消协程
[job] CancellationException StandaloneCoroutine was cancelled
[job][finally] 进入NonCancellable
[job][finally] NonCancellable完毕
[job][finally] 结束
[rustfisher] 程序退出 bye~

注意:NonCancellable只能与withContext搭配使用。

超时

我们可以用withTimeout(long)来指定超时时间。

例如下面代码,指定了超时时间

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    launch {
        withTimeout(1300L) {
            val startTime = System.currentTimeMillis()
            repeat(1000) { i ->
                println("[job] 运行: $i, 累积运行时间: ${System.currentTimeMillis() - startTime}毫秒")
                delay(500L)
            }
        }
    }
}
运行log
[job] 运行: 0, 累积运行时间: 0毫秒
[job] 运行: 1, 累积运行时间: 506毫秒
[job] 运行: 2, 累积运行时间: 1011毫秒

在原来代码上加上try-catch,观察超时抛出的异常

fun main() = runBlocking<Unit> {
    launch {
        try {
            withTimeout(400L) {
                val startTime = System.currentTimeMillis()
                repeat(1000) { i ->
                    println("[job] 运行: $i, 累积运行时间: ${System.currentTimeMillis() - startTime}毫秒")
                    delay(100L)
                }
            }
        } catch (e: Exception) {
            println("异常: $e")
        }
    }
}
运行log
[job] 运行: 0, 累积运行时间: 0毫秒
[job] 运行: 1, 累积运行时间: 105毫秒
[job] 运行: 2, 累积运行时间: 209毫秒
[job] 运行: 3, 累积运行时间: 312毫秒
异常: kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 400 ms
可以看到超时抛出的是TimeoutCancellationException,它是CancellationException的子类。 前面的代码没在控制台看到这个异常,是因为在被取消的协程中CancellationException被认为是协程执行结束的正常原因。我们可以主动捕获它。

withTimeoutOrNull方法会在超时后返回null,如果成功执行则返回我们指定的东西,如下面代码

fun main() = runBlocking<Unit> {
    launch {
        val result1 = withTimeoutOrNull(1300L) {
            repeat(1000) { i ->
                println("[job1] 运行 $i ...")
                delay(500L)
            }
            "[1] Done" // 根据超时设置 执行不到这里
        }
        println("Result1: $result1")

        val result2 = withTimeoutOrNull(1300L) {
            repeat(2) { i ->
                println("[job2] 运行 $i ...")
                delay(500L)
            }
            "[2] Done" // 成功执行完毕后到这里
        }
        println("Result2: $result2")
    }
}

运行log

[job1] 运行 0 ...
[job1] 运行 1 ...
[job1] 运行 2 ...
Result1: null
[job2] 运行 0 ...
[job2] 运行 1 ...
Result2: [2] Done

本文也发布在

华为云社区

本站说明

一起在知识的海洋里呛水吧。广告内容与本站无关。如果喜欢本站内容,欢迎投喂作者,谢谢支持服务器。

AndroidTutorial AndroidTutorial 反馈问题 讨论区 最近更新 投喂作者

Ads