github原文地址
原创翻译,转载请保留或注明出处:https://www.jianshu.com/p/d9d9bfd0be55
取消和超时
该小节涵盖协程的取消和超时的内容
取消协程的执行
在小的应用程序中,从主函数返回可能听起来像是一个好主意,让所有的协程都隐式的终止。在一个更大的、长期运行的应用程序中,你需要更细粒度的控制。launch 函数返回 Job 对象,它可以用来取消运行中的协程:
fun main(args: Array) = runBlocking {
val job = launch {
repeat(1000) { i ->
println("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.")
}
获取完整代码 here
这段代码输出如下:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.
只要主函数调用了job.cancel
,我们就看不到其他协程的任何输出。还有一个 Job 的扩展函数 cancelAndJoin,它结合了 cancel 和 join 调用。
取消是协同的
协程取消是协同进行的。协程代码必须配合才能被取消。kotlinx.coroutines
中的挂起函数都是可取消的。它们检查协程是否被取消,并在取消时抛出 CancellationException 异常。然而当协程正在进行计算工作而且未检查取消状态,则不能取消,如以下代码所示:
fun main(args: Array) = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch {
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("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.")
}
获取完整代码 here
运行这段代码会发现,取消后协程仍在打印“I'm sleeping”,直到任务在五次迭代后自行完成任务。
使计算代码可取消
有两种方式使计算代码可取消。第一种方式是周期性地调用一个检查取消的挂起函数,yield 函数是一个很好的选择。另一种是明确地检查取消状态,我们尝试一下后者:
使用while (isActive)
替代之前示例中的while (i < 5)
,然后返回。
fun main(args: Array) = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch {
var nextPrintTime = startTime
var i = 0
while (isActive) { // cancellable computation loop
// print a message twice a second
if (System.currentTimeMillis() >= nextPrintTime) {
println("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.")
}
获取完整代码 here
可以看到,这个循环被取消了。isActive 是一个在协程代码中可用的属性,通过 CoroutineScope 对象。
使用finally关闭资源
当取消可取消的挂起函数时抛出的 CancellationException,可以用所有通常的方式处理。例如,当协程取消时,try {...} finally {...}
表达式和use
函数正常执行终止动作:
fun main(args: Array) = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
} finally {
println("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.")
}
获取完整代码 here
join 和 cancelAndJoin 均会等待终止动作的完成,所以上面的示例输出如下:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
main: I'm tired of waiting!
I'm running finally
main: Now I can quit.
运行不可取消的代码块
前一个示例中,任何在 finally
代码块中使用挂起函数的尝试都会引发 CancellationException,因为运行这段代码的协程是可取消的。通常这不是一个问题,因为所有行为良好的关闭动作(关闭文件,取消作业或关闭任何类型的通信通道)通常都是非阻塞的,并且不涉及任何挂起函数。然而在极少数情况下,当你需要在已经取消的协程中挂起时,可以使用 withContext 函数和 withContext 上下文将相应的代码封装在withContext(NonCancellable) {...}
中,如下所示:
fun main(args: Array) = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
} finally {
withContext(NonCancellable) {
println("I'm running finally")
delay(1000L)
println("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.")
}
获取完整代码 here
超时
在实践中取消协程执行的最明显的原因,是因为它已经执行超过了一段时间。虽然你可以手动追踪相应作业的引用,并启动一个单独的协程用来在延迟后取消被追踪的协程,但更好的选择是使用 withTimeout 函数来做这件事。如下所示:
fun main(args: Array) = runBlocking {
withTimeout(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
}
获取完整代码 here
输出如下:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Exception in thread "main" kotlinx.coroutines.experimental.TimeoutCancellationException: Timed out waiting for 1300 MILLISECONDS
withTimeout 抛出的TimeoutCancellationException
异常是 CancellationException 的一个子类。我们之前没有看到它的堆栈轨迹打印在控制台上。这是因为在一个被取消的协程中,CancellationException
被认为是协程完成的正常原因。然而,在这个示例中我们已经在主函数内部使用了withTimeout
。
因为取消动作只是一个异常,所有的资源将以通常的方式被关闭。如果你需要在任何类型的超时发生时做一些额外的操作,你可以将带有超时的代码封装在try {...} catch (e: TimeoutCancellationException) {...}
中,或者使用 withTimeoutOrNull 函数,它类似于 withTimeout,但是在超时发生时返回null
而不是异常:
fun main(args: Array) = runBlocking {
val result = withTimeoutOrNull(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
"Done" // will get cancelled before it produces this result
}
println("Result is $result")
}
获取完整代码 here
运行这段代码将不会再发生异常:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Result is null