协程的取消与超时
1.取消协程的执行
一个长时间运行的应用程序,你可能需要细粒度的控制后台协程。例如,你已经关闭了一个启动协程的页面,该协程的运行结果已经不再需要,该协程可以取消其操作。launch函数返回了job实例,可以使正在运行的协程取消。
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
repeat(1000) { i ->
println("job: 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.")
}
输出如下:
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.
不久后,我们在主线程调用了job.cancel,我们没有再看到来自其他协程的任何输出,因为它被取消了。这里也有一个协程的扩展方法cancelAndJoin,它结合了cancel和join方法的调用。
2.取消是协作的
协程的取消是协作的,一个协程代码必须协作取消。所有在kotlinx.coroutines包下面的挂起函数都可以取消。它们检查协程的取消并抛出 CancellationException 当协程取消的时候。然后,当一个协程正在执行计算密集型操作其不会检查取消,其不会被取消。像下面代码展示的那样:
import kotlinx.coroutines.*
fun main() = runBlocking {
val startTime = System.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 (System.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.")
}
结果如下
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm sleeping 3 ...
job: I'm sleeping 4 ...
main: Now I can quit.
运行后发现,在协程取消后 I’m sleeping 继续打印,一直到job完成5次循环后结束。
3.使计算密集型操作可以被取消
有两种方法可以使计算密集型操作被取消。第一种方法是定期调用一个挂起函数检查协程是否取消。这里有个一yeild是一个很好的选择。另一个方法是指定检查协程的取消状态。先试一下最后一个方法。
再上一个例子中,替换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) { // cancellable computation loop
// print a message twice a second
if (System.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.")
}
运行结果
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.
如你所见,现在这个循环取消了,isActive是一个扩展属性,可以用于协程标签为 CoroutineScope 的实例中。
4.在finally中关闭资源
可取消的挂起函数在取消时会抛出 CancellationException,这可以按通常的方式处理。例如,try {…} finally {…} 表达式和 Kotlin 使用函数在协程被取消时正常执行它们的终结操作:
import kotlinx.coroutines.*
fun main() = 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和joinAndCancel等待所有最终的动作完成。所以上面的例子产生下面的输出结果:
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm running finally
main: Now I can quit.
5.运行不被取消的代码块
任何在上一个示例的 finally 块中使用挂起函数的尝试都会导致 CancellationException,因为运行此代码的协程被取消。通常,这不是问题,因为所有行为良好的关闭操作(关闭文件、取消作业或关闭任何类型的通信通道)通常都是非阻塞的,并且不涉及任何挂起功能。但是,在极少数情况下,当您需要在取消的协程中挂起时,您可以使用 withContext 函数和 NonCancellable 上下文将相应的代码包装在 withContext(NonCancellable) {…} 中,如下例所示:
import kotlinx.coroutines.*
fun main() = 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.")
}
运行结果如下
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm running finally
job: And I've just delayed for 1 sec because I'm non-cancellable
main: Now I can quit.
5.超时
取消协程执行的最明显的实际原因是它的执行时间已经超过了一些时间限制。虽然您可以手动跟踪对相应 Job 的引用并启动一个单独的协程以在延迟后取消跟踪的协程,有一个使用的 withTimeout 函数已经可以做到这一点。看下面的例子:
import kotlinx.coroutines.*
fun main() = 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
TimeoutCancellationException被withTimeout函数抛出,它是CancellationException的一个子类。我们之前没有在控制台上看到它的堆栈跟踪打印。那是因为在取消的协程内部 CancellationException 被认为是协程完成的正常原因。然而,在这个例子中,我们在 main 函数中使用了 withTimeout
由于取消只是一个例外,所有资源都以通常的方式关闭。 您可以在try{…}中用timeout包装代码捕获(e:TimeoutCancellationException){…}如果需要针对任何类型的超时执行一些额外的操作,或者使用类似于withTimeout的WithTimeOutorNull函数,但在超时时返回null,而不是引发异常,请执行以下操作:
import kotlinx.coroutines.*
fun main() = 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")
}
结果如下
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Result is null
可以看到超时后不会返回Done,而是返回了null
6.异步超时和资源
withTimeout 中的超时事件与其块中运行的代码是异步的,并且随时可能发生,甚至在从超时块内部返回之前。如果您在块内打开或获取一些需要在块外关闭或释放的资源,请记住这一点。
例如,我们仿写一个可以关闭资源的Resource类,它只是通过递增获取的计数器并从其关闭函数中递减该计数器来跟踪它被创建的次数。
让我们运行大量具有小超时的协程,尝试在经过一点延迟后从 withTimeout 块内部获取此资源并从外部释放它。
import kotlinx.coroutines.*
var acquired = 0
class Resource {
init { acquired++ } // Acquire the resource
fun close() { acquired-- } // Release the resource
}
fun main() {
runBlocking {
repeat(100_000) { // Launch 100K coroutines
launch {
val resource = withTimeout(60) { // Timeout of 60 ms
delay(50) // Delay for 50 ms
Resource() // Acquire a resource and return it from withTimeout block
}
resource.close() // Release the resource
}
}
}
// Outside of runBlocking all coroutines have completed
println(acquired) // Print the number of resources still acquired
}
如果你运行上面的代码,你将会看到其输出并不总是为零,尽管它可能取决于您机器的时间,但您可能需要在此示例中调整超时以实际看到非零值。
要解决此问题,您可以在变量中存储对资源的引用,而不是从 withTimeout 块中返回它。
import kotlinx.coroutines.*
var acquired = 0
class Resource {
init { acquired++ } // Acquire the resource
fun close() { acquired-- } // Release the resource
}
fun main() {
runBlocking {
repeat(100_000) { // Launch 100K coroutines
launch {
var resource: Resource? = null // Not acquired yet
try {
withTimeout(60) { // Timeout of 60 ms
delay(50) // Delay for 50 ms
resource = Resource() // Store a resource to the variable if acquired
}
// We can do something else with the resource here
} finally {
resource?.close() // Release the resource if it was acquired
}
}
}
}
// Outside of runBlocking all coroutines have completed
println(acquired) // Print the number of resources still acquired
}
这个例子总是打印零。资源不泄漏。