学习协程2: 取消和超时

文章介绍了Kotlin协程的取消机制,包括如何通过job对象取消协程并捕获CancellationException。还讨论了协程的协作取消性质,以及如何通过yield()或检查isActive状态使计算任务可取消。此外,提到了withTimeout函数用于超时控制,以及如何处理资源关闭和异步超时问题。
摘要由CSDN通过智能技术生成

Kotlin协程官方网址

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

两种方法可以取消计算工作的协程。

  1. 第一种方法是定期调用检查取消的挂起函数。有一个yield函数是一个很好的选择。
  2. 另一个是显式检查取消状态。

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抛出的TimeoutCancellationExceptionCancellationException的子类。以前没有在控制台上看到它的堆栈跟踪。这是因为在取消的协程中,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函数中的计数器来跟踪它被创建的次数。现在让我们创建许多协程,每个协程在withTimeout块的末尾创建一个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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值