超时与取消

介绍协程的取消和超时

取消协程执行

你也许需要对于长时间执行应用后台协程的细粒度控制 可以通过对于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.")   
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

取消是需要协调的

协程的代码必须检查协程取消状态才能是可取消的

所有在kotlinx.coroutinesuspend函数都是可取消的 会检查协程取消状态 取消时会抛出CancellationException 上例中的 delay 函数是 suspend 函数

一个不检查取消状态的协程是不会被取消的

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.")
    
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

协程会打印所有5次信息

让代码可取消

有两种方法可以让协程的代码能够被取消

这里展示后者 用while(isActive) 替换之前代码的 while(i<5)

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.")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

协程中取消的处理

可以在协程中添加 catchfinally 块来处理取消所产生的异常

import kotlinx.coroutines.*

fun main() = runBlocking {

    val job = launch {
        try {
            repeat(1000) { i ->
                println("job: I'm sleeping $i ...")
                delay(500L)
            }
        } catch(c:CancellationException){
            println("Catch CancellationException")
        }
        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.")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

joincancelAndJoin 都会等待 finally 块的执行

不能取消的代码块

因为执行代码的协程被取消 所以在 finally 块中执行 suspend 函数会导致 CancellationException 异常的抛出

使用 withContext(NonCancellable){} 代码块来保证不会被取消

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.")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

超时

在协程域中 可以设置函数的超时 Kotlin Playgroundopen in new window

withTimeout(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
1
2
3
4
5
6

超时时会抛出 TimeoutCancellationException 这是 CancellationException 的一个子类

取消(cancellation)只是一种异常 所以可以用 try-catch-finally 包起来处理 也可以使用 withTimeoutOrNull 处理需要返回值的情况

该函数在超时时返回null而不抛出异常

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")
1
2
3
4
5
6
7
8

异步超时

withTimeout 中的超时事件相对于在其块中运行的代码是异步的,并且可能随时发生,甚至在从超时块内部返回之前

所以在协程中使用资源时 在注意在finally块中释放资源

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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

例如执行上述一段代码 最后不一定输出0跟循环多加一两个数量级结果更明显

TIP

因为都发生在同一个主线程 所以100K个协程对于acquired的加减是完全安全的

更多的内容会在下一章的 coroutine context 中讲解

解决上述问题只需要把上述launch中的代码修改为这样

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
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

这样就能保证输出0了