Kotlin学习之协程的取消与超时

这一部分包含了协程的取消与超时。

取消协程的执行

在一个⻓时间运行的应用程序中,你也许需要对你的后台协程进行细粒度的控制。 比如说,一个用戶也许关闭了一个启动了协程的界面,那么现在协程的执行结果已经不再被需要了,这时,它应该是可以被取消的。 该 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

你可能感兴趣的:(Kotlin学习之协程的取消与超时)