超时与取消
介绍协程的取消和超时
取消协程执行
你也许需要对于长时间执行应用后台协程的细粒度控制 可以通过对于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.")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
取消是需要协调的
协程的代码必须检查协程取消状态才能是可取消的
所有在
kotlinx.coroutine
的suspend
函数都是可取消的 会检查协程取消状态 取消时会抛出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.")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
协程会打印所有5次信息
让代码可取消
有两种方法可以让协程的代码能够被取消
- 周期性调用检查取消的
suspend
函数(比如yield
) - 显式检查取消状态
这里展示后者 用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.")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
协程中取消的处理
可以在协程中添加 catch
和 finally
块来处理取消所产生的异常
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.")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
join
和cancelAndJoin
都会等待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.")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
超时
在协程域中 可以设置函数的超时 Kotlin Playground
withTimeout(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
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")
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
}
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
}
}
2
3
4
5
6
7
8
9
10
11
12
这样就能保证输出0了