Kotlin协程取消与超时
开发环境
- IntelliJ IDEA 2021.2.2 (Community Edition)
- Kotlin: 212-1.5.10-release-IJ5284.40
这里介绍协程的超时和取消。
取消
我们可以启动协程,也可以在协程尚未结束时,主动取消协程。
例如在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可以看出在它没有输出了,因为它被取消了。
我们也可以调用Job的cancelAndJoin()
方法来代替上面代码中的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
- 点赞
- 收藏
- 关注作者
评论(0)