Cancelling coroutine execution
对于长时间运行的程序,需要进行粒度控制,在合适的时间结束协程。launch
返回一个job
对象,可以使用该对象取消正在运行的协程。取消是抛出 CancellationException
,如果不捕捉,协程被取消。
fun cancelCoroutine() = 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.")
}
// output
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.
开启一个协程开始打印,没打印一次延时500ms,整个线程延时1300ms, 因此打印三次。然后取消协程,等待协程结束后打印最后的语句。
Cancellation is cooperative
取消是协作的,一个协程可以被取消必须是与其他协作。kotlinx.coroutines
中的挂起函数都是可取消的。但是,如果协程在计算中工作并且不检查取消,则无法取消。如下:
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.")
//output
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.
同样的问题可以观察到通过捕捉 CancellationException
val job = launch(Dispatchers.Default) {
repeat(5) { i ->
try {
// print a message twice a second
println("job: I'm sleeping $i ...")
delay(500)
} catch (e: Exception) {
// log the exception
println(e)
}
}
}
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.")
// output
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@23ff831c
job: I'm sleeping 3 ...
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@23ff831c
job: I'm sleeping 4 ...
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@23ff831c
main: Now I can quit.
在取消协程是会抛出 CancellationException
,但是,如果协程在计算中工作并且不检查取消,则无法取消。
Making computation code cancellable
两种方法可以取消计算工作的协程。
- 第一种方法是定期调用检查取消的挂起函数。有一个
yield
函数是一个很好的选择。 - 另一个是显式检查取消状态。
use yield
如果要处理的任务属于 1) CPU 密集型,2) 可能会耗尽线程池资源,3) 需要在不向线程池中添加更多线程的前提下允许线程处理其他任务,那么请使用 yield()。如果 job 已经完成,由 yield 所处理的首要任务将会是检查任务的完成状态,完成的话则直接通过抛出 CancellationException
来退出协程。yield 可以作为定期检查所调用的第一个函数
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
yield()
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.")
// out put
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.
explicitly check the cancellation status
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5 && isActive) { // 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.")
//output
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.
Closing resources with finally
可取消挂起函数在取消时抛出CancellationException
,这可以用通常的方式处理。例如,try{…} finally{…}
表达式和Kotlin的use函数在协程被取消时正常执行它们的结束动作。
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.")
//output
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm running finally
main: Now I can quit.
Run non-cancellable block
在finally
中使用任何挂起函数都会造成 CancellationException
, 因为协程已经被取消了。如果需要在取消的协程中挂起,使用withContext(NonCancellable) {...}
使用 withContext
函数和NonCancellable context
。
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.")
// output
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm running finally
job: And I've just delayed for 1 sec because I'm non-cancellable
main: Now I can quit.
Timeout
使用 withTimeout
在超时后取消协程。
withTimeout(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
//output
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
。由于取消只是一个异常,所以所有资源都以通常的方式关闭。可以在try{…} catch (e: TimeoutCancellationException){…}
块,如果需要在任何类型的超时时执行一些额外的操作,或者使用与withTimeout
类似的withTimeoutOrNull
函数,但它在超时时返回null
,而不是抛出异常。
try {
withTimeout(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
} catch (e:TimeoutCancellationException) {
println(e.message)
}
println("this is end")
// output
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Timed out waiting for 1300 ms
this is end
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")
// output
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Result is null
Asynchronous timeout and resources
withTimeout
中的超时事件相对于在其块中运行的代码是异步的,并且可以在任何时间发生,甚至在从超时块内部返回之前发生。如果在块内部打开或获取一些需要在块外部关闭或释放的资源,请记住这一点。例如,这里我们用resource
类模拟一个可关闭的资源,它只是通过增加获取的计数器和减少其close
函数中的计数器来跟踪它被创建的次数。现在让我们创建许多协程,每个协程在withTimeou
t块的末尾创建一个Resource
,并在块外部释放资源。我们添加了一个小延迟,以便在withTimeout
块已经完成时更有可能发生超时,这将导致资源泄漏。
var acquired = 0
class Resource {
init { acquired++ } // Acquire the resource
fun close() { acquired-- } // Release the resource
}
fun main() {
runBlocking {
repeat(10_000) { // Launch 10K 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
}
//output
1103
要解决这个问题,可以将对资源的引用存储在变量中,而不是从withTimeout
块返回它。
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
//output
0