在应用程序中,我们需要对协程进行细粒度控制。例如,用户可能已经关闭了启动协程的页面,不再需要其结果,因此可以取消协程。
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.
这个很简单。
协程取消是合作式的
下面我们来点有难度的,上一篇文章我们说到协程的取消是合作式的,现在我们来验证一下。
@JvmStatic
fun main2(args: Array<String>) = 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.")
}
协程中,我们每隔 500ms 会打印一句话,协程外面,我们在 1300ms 后取消了协程。按照道理来说,协程应该只会打印3次才对。但是输出结果是:
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.
说明我们没能成功取消协程,这是为啥呢?
是因为协程的取消是通过 JobCancellationException 来完成的,而这个协程里面并没有处理改异常,所以就不会取消。
我们换一种写法:
@JvmStatic
fun main(args: Array<String>) = runBlocking {
val job = launch(Dispatchers.Default) {
repeat(5) { i ->
// print a message twice a second
println("job: I'm sleeping $i ...")
delay(500)
}
}
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.")
}
这种方式就可以正常取消。其原因就在于 delay 函数是可以被取消的。delay 是一个挂起函数,它会影响协程挂起点后面的执行流逻辑,所以整个协程就是可取消的了。
那么,我们有没有什么办法能够让while里面的代码也可以取消呢?也是可以的:
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.")
}
这里使用了 isActive 字段来判断协程是否被取消了。
取消协程时关闭资源
当一个协程被取消的时候,我们想要关闭一些资源,避免资源泄漏。可以使用 finally 语句:
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.")
}
finally 里面可以释放资源,但是要注意,不能在 finally 里面搞延迟操作,因为执行不到。比如:
@JvmStatic
fun main(args: Array<String>) = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L)
}
} finally {
println("--------")
delay(1000)
println("--------")
// 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!
--------
main: Now I can quit.
如果非要搞延迟操作,那就使用 NonCancellable,将上面注释代码打开即可。
给协程设置超时
取消协程的一个普遍的原因是因为协程超时了。有一个便利的API可以直接设置超时自动取消:
withTimeout(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
运行发现,它会抛出一个异常。 TimeoutCancellationException 是 CancellationException 的一个子类。
想要不抛出异常,可以使用另一个方法:
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")
超时资源处理
当遇到超时取消情况,并且我们还使用一些资源,那么就需要想办法合理的关闭资源。
看一个例子:
var acquired = 0
class Resource {
init { acquired++ } // Acquire the resource
fun close() { acquired-- } // Release the resource
}
我们用到了一个 Resource 类。
fun main() {
runBlocking {
repeat(10_000) { // Launch 10K coroutines
launch {
val resource = withTimeout(51) { // 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
}
我们开了1w个协程,协程的超时设置为 51ms。由于协程调度的原因,并不是每个协程都能在 51 ms 之内完成。所以会导致有些协程的资源没有正常关闭。
一个办法就是使用 finally 语句:
runBlocking {
repeat(10_000) { // Launch 10K 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