【翻译】kotlin协程核心库文档(二)—— 取消和超时

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

你可能感兴趣的:(【翻译】kotlin协程核心库文档(二)—— 取消和超时)