kotlin coroutine文档:取消和超时

取消和超时

这部分讲述协程的取消和超时

取消协程的执行

在一个长期运行的应用,你可能需要细粒度地控制后台协程。例如,一个用户可能关闭页面,这个页面启动了一个协程,它的结果现在不再需要了,它的运行应该要取消。launch函数返回一个Job,它可以用来取消运行的协程:

import kotlinx.coroutines.*

fun main() = runBlocking {
//例子开始
    val job = launch {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // 延迟一会儿
    println("main: I'm tired of waiting!")
    job.cancel() // 取消一个job
    job.join() // 等待job结束
    println("main: Now I can quit.")
//例子结束  
}

这里可以获取完整代码

运行结果如下:

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,它结合了canceljoin调用。

取消是协作的

协程取消是协作的(cooperative)。为了可取消,协程代码必须是协作的。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) { // 计算循环,仅仅为了浪费CPU
            // 一秒打印信息两次
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L) // 延迟一会儿
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // 取消job,等待它结束
    println("main: Now I can quit.")
//例子结束    
}

这里可以获取完整代码

运行它可以发现,它不停打印“I’m sleeping”,即使在取消之后,直至经过五个迭代之后job自己结束。

使得计算代码可取消

有两个方法可以让计算代码可取消。第一个是定期地调用一个挂起函数,这个函数检查可取消。有一个yield函数,是达到这个目的一个很好的选择。另外一个显式地检查可取消状态。让我们试试后一种的方法。

用 while (isActive) 代替前面例子中的 while (i < 5),然后从新运行

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("I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L) // 延迟一会儿
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // 取消job,等待它结束
    println("main: Now I can quit.")
//例子结束    
}

这里可以获取完整代码

就像你看见的,这个循环现在取消了,通过CoroutineScope对象,isActive是一个在协程代码里面可以获取到的扩展属性。

用finally关闭资源

可取消的挂起函数在取消时抛出CancellationException,可以用一个更加通用的方式处理。例如, 通常在协程取消的时候,try {…} finally {…} 表达式、Kotlin的use函数执行它们的终结任务:

import kotlinx.coroutines.*

fun main() = runBlocking {
//例子开始
    val job = launch {
        try {
            repeat(1000) { i ->
                println("I'm sleeping $i ...")
                delay(500L)
            }
        } finally {
            println("I'm running finally")
        }
    }
    delay(1300L) // 延迟一会儿
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() //取消job,等待它结束
    println("main: Now I can quit.")
//例子结束    
}

这里可以获取完整代码

joincancelAndJoin两者都会等待所有终结任务结束,所以上面例子得出如下结果:

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,这是因为运行这段代码的协程被取消了。通常这不是一个问题,因为所有良好运行的关闭操作(关闭一个文件、取消一个job,或者关闭任何类型的通信通道)通常是非阻塞的,不会包含任何挂起函数。然而,极端情况下,当你需要在取消协程中挂起,你可以用使用withContextNonCancellable上下文,把相关代码包含到withContext(NonCancellable) {…}里面,,就像下面例子所示:

import kotlinx.coroutines.*

fun main() = 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) // 延迟一会儿
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() //取消job,等待它结束
    println("main: Now I can quit.")
//例子结束    
}

这里可以获取完整代码

超时

在实践中,取消协程执行的最明显理由是,它的执行时间超过了某个时限。虽然你可以手动地追踪相应Job的引用,然后启动独立的协程在一定时限后取消跟踪的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

withTimeout抛出的TimeoutCancellationException,是CancellationException的子类。我们从没有在控制台上看见打印过它的堆栈。这是因为在一个取消的协程内部,CancellationException被认为是协程结束的一个合理理由。然而这个例子中,我们正是在main函数里面使用了withTimeout。

因为取消仅仅是一个异常,所有资源是在正常情况下关闭的。你可以在try {…} catch (e: TimeoutCancellationException) {…}代码块中包含超时的代码,如果你需要做一些额外的操作,特别是对某种超时或者使用withTimeoutOrNull函数,这个函数类似于withTimeout,但是在超时时返回null而不是抛出一个异常:

import kotlinx.coroutines.*

fun main() = runBlocking {
//例子开始
    val result = withTimeoutOrNull(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
        "Done" // 将在产生这个结果之前得到取消
    }
    println("Result is $result")
//例子结束
}

这里可以获取完整代码

当运行这段代码时,不再是一个异常:

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Result is null

猜你喜欢

转载自blog.csdn.net/tigershin/article/details/86409562