这一部分包含了协程的取消与超时。
取消协程的执行
在一个⻓时间运行的应用程序中,你也许需要对你的后台协程进行细粒度的控制。 比如说,一个用戶也许关闭了一个启动了协程的界面,那么现在协程的执行结果已经不再被需要了,这时,它应该是可以被取消的。 该 launch 函数返回了一个可以被用来取消运行中的协程的 Job:
fun main() = runBlocking {
val job = launch {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L)
}
}
delay(5300L) // 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.")
}
程序执行后的输出如下:
2020-05-12 11:43:48.197 14605-14605/com.yy.kotlindemo I/System.out: job: I'm sleeping 1 ...
2020-05-12 11:43:48.732 14605-14605/com.yy.kotlindemo I/System.out: job: I'm sleeping 2 ...
2020-05-12 11:43:49.263 14605-14605/com.yy.kotlindemo I/System.out: job: I'm sleeping 3 ...
2020-05-12 11:43:49.806 14605-14605/com.yy.kotlindemo I/System.out: job: I'm sleeping 4 ...
2020-05-12 11:43:50.348 14605-14605/com.yy.kotlindemo I/System.out: job: I'm sleeping 5 ...
2020-05-12 11:43:50.891 14605-14605/com.yy.kotlindemo I/System.out: job: I'm sleeping 6 ...
2020-05-12 11:43:51.433 14605-14605/com.yy.kotlindemo I/System.out: job: I'm sleeping 7 ...
2020-05-12 11:43:51.944 14605-14605/com.yy.kotlindemo I/System.out: job: I'm sleeping 8 ...
2020-05-12 11:43:52.478 14605-14605/com.yy.kotlindemo I/System.out: job: I'm sleeping 9 ...
2020-05-12 11:43:53.011 14605-14605/com.yy.kotlindemo I/System.out: main: I'm tired of waiting!
2020-05-12 11:43:53.016 14605-14605/com.yy.kotlindemo I/System.out: main: Now I can quit.
一旦 main 函数调用了 job.cancel ,我们在其它的协程中就看不到任何输出,因为它被取消了。 这里也有一个可以使 Job 挂起的函数 cancelAndJoin 它合并了对 cancel 以及 join 的调用。
取消是协作的
协程的取消是 协作 的。一段协程代码必须协作才能被取消。 所有 kotlinx.coroutines 中的挂起函数都是 可被取消的 。它们检查协程的取消, 并在取消时抛出 CancellationException。 然而,如果协程正在执行计算任务,并且没有检查取消的话,那么它是不能被取消的,就如如下示例代码所示:
fun main() = runBlocking {
val startTime = 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 (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.")
}
2020-05-12 12:06:21.211 17266-17457/com.yy.kotlindemo I/System.out: job: I'm sleeping 0 ...
2020-05-12 12:06:21.697 17266-17457/com.yy.kotlindemo I/System.out: job: I'm sleeping 1 ...
2020-05-12 12:06:22.197 17266-17457/com.yy.kotlindemo I/System.out: job: I'm sleeping 2 ...
2020-05-12 12:06:22.552 17266-17266/com.yy.kotlindemo I/System.out: main: I'm tired of waiting!
2020-05-12 12:06:22.697 17266-17457/com.yy.kotlindemo I/System.out: job: I'm sleeping 3 ...
2020-05-12 12:06:23.197 17266-17457/com.yy.kotlindemo I/System.out: job: I'm sleeping 4 ...
2020-05-12 12:06:23.201 17266-17266/com.yy.kotlindemo I/System.out: main: Now I can quit.
运行示例代码,并且我们可以看到它连续打印出了“I'm sleeping”,甚至在调用取消后, 作业仍然执行了五次循环迭代并运行到了它结束为止。
使计算代码可取消
我们有两种方法来使执行计算的代码可以被取消。第一种方法是定期调用挂起函数来检查取消。对于这种目的 yield 是一个好的选择。 另一种方法是显式的检查取消状态。让我们试试第二种方法。
将前一个示例中的 while (i < 5) 替换为 while (isActive) 并重新运行它。
fun main() = runBlocking {
val startTime = currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (isActive) { // cancellable computation loop
// print a message twice a second
if (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.")
}
程序执行
2020-05-12 13:58:53.109 31146-31352/com.yy.kotlindemo I/System.out: job: I'm sleeping 1 ...
2020-05-12 13:58:53.610 31146-31352/com.yy.kotlindemo I/System.out: job: I'm sleeping 2 ...
2020-05-12 13:58:53.962 31146-31146/com.yy.kotlindemo I/System.out: main: I'm tired of waiting!
2020-05-12 13:58:53.967 31146-31146/com.yy.kotlindemo I/System.out: main: Now I can quit.
你可以看到,现在循环被取消了。isActive 是一个可以被使用在 CoroutineScope 中的扩展属性。
在 finally 中释放资源
我们通常使用如下的方法处理在被取消时抛出 CancellationException 的可被取消的挂起函数。比如
说,try {......} finally {......} 表达式以及 Kotlin 的 use 函数一般在协程被取消的时候执行它们的终结动作:
fun main12() = 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.")
}
join 和 cancelAndJoin 等待了所有的终结动作执行完毕, 所以运行示例得到了下面的输出:
2020-05-12 14:05:45.691 595-595/com.yy.kotlindemo I/System.out: job: I'm sleeping 0 ...
2020-05-12 14:05:46.194 595-595/com.yy.kotlindemo I/System.out: job: I'm sleeping 1 ...
2020-05-12 14:05:46.737 595-595/com.yy.kotlindemo I/System.out: job: I'm sleeping 2 ...
2020-05-12 14:05:46.999 595-595/com.yy.kotlindemo I/System.out: main: I'm tired of waiting!
2020-05-12 14:05:47.013 595-595/com.yy.kotlindemo I/System.out: job: I'm running finally
2020-05-12 14:05:47.015 595-595/com.yy.kotlindemo I/System.out: main: Now I can quit.
运行不能取消的代码块
在前一个例子中任何尝试在 finally 块中调用挂起函数的行为都会抛出 CancellationException,因为这里持续运行的代码是可以被取消的。通常,这并不是一个问题,所有良好的关闭操作(关闭一个文件、取消一个作业、或是关闭任何一种通信通道)通常都是非阻塞的,并且不会调用任何挂起函数.然而,在真实的案例中,当你需要挂起一个被取消的协程,你可以将相应的代码包装在withContext(NonCancellable) {.} 中,并使用 withContext 函数以及NonCancellable 上下文,⻅如下示例所示:
fun main13() = 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.")
}
程序执行结果:
2020-05-12 14:13:58.130 1902-1902/com.yy.kotlindemo I/System.out: job: I'm sleeping 0 ...
2020-05-12 14:13:58.641 1902-1902/com.yy.kotlindemo I/System.out: job: I'm sleeping 1 ...
2020-05-12 14:13:59.184 1902-1902/com.yy.kotlindemo I/System.out: job: I'm sleeping 2 ...
2020-05-12 14:13:59.454 1902-1902/com.yy.kotlindemo I/System.out: main: I'm tired of waiting!
2020-05-12 14:13:59.463 1902-1902/com.yy.kotlindemo I/System.out: job: I'm running finally
2020-05-12 14:14:00.478 1902-1902/com.yy.kotlindemo I/System.out: job: And I've just delayed for 1 sec because I'm non-cancellable
2020-05-12 14:14:00.482 1902-1902/com.yy.kotlindemo I/System.out: main: Now I can quit.
超时
在实践中绝大多数取消一个协程的理由是它有可能超时。 当你手动追踪一个相关 Job 的引用并启动了一个单独的协程在延迟后取消追踪,这里已经准备好使用 withTimeout 函数来做这件事。 来看看示例代码:
fun main14() = 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
withTimeout 抛出了 TimeoutCancellationException ,它是 CancellationException 的子类。我们之前没有在控制台上看到堆栈跟踪信息的打印。这是因为在被取消的协程中CancellationException 被认为是协程执行结束的正常原因。 然而,在这个示例中我们在 main函数中正确地使用了 withTimeout 。
由于取消只是一个例外,所有的资源都使用常用的方法来关闭。 如果你需要做一些各类使用超时的特别的额外操作,可以使用类似 withTimeout 的 withTimeoutOrNull 函数,并把这些会超时的代码包装在 try {...} catch (e: TimeoutCancellationException) {...} 代码块中,而withTimeoutOrNull 通过返回 null 来进行超时操作,从而替代抛出一个异常:
fun main15() = runBlocking {
val result = withTimeoutOrNull(1300L) {
repeat(1000) { i ->
println("CoroutineTest I'm sleeping $i ...")
delay(500L)
}
"Done" // will get cancelled before it produces this result
}
println("CoroutineTest Result is $result")
}
运行这段代码时不再抛出异常,执行结果:
2020-05-13 10:37:43.191 6243-6243/com.yy.kotlindemo I/System.out: CoroutineTest I'm sleeping 0 ...
2020-05-13 10:37:43.692 6243-6243/com.yy.kotlindemo I/System.out: CoroutineTest I'm sleeping 1 ...
2020-05-13 10:37:44.193 6243-6243/com.yy.kotlindemo I/System.out: CoroutineTest I'm sleeping 2 ...
2020-05-13 10:37:44.493 6243-6243/com.yy.kotlindemo I/System.out: CoroutineTest Result is null