当使用协程进行异步任务的时候,往往也会因为一些情况对其进行取消。取消异步任务通常使用Job.cancel()
函数
对于cancel()
的使用方式如下:
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.")
}
那么运行结果如下:
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.
由此可以看出来在协程取消后程序也就自动停止了。
协程理应是可以取消的,但如果我们在协程里面做一些复杂的操作并且不进行检查,那么即使协程进行了取消操作,但是里面的任务依然会执行完毕。
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.")
}
执行结果如下
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.
可以看出已经取消的任务,最后依然执行了剩下的操作。所以这里面有两种解决方案,一种是使用yield()
函数,另外一种就是见擦好自身的血橙状态。这里使用第二种方式
import kotlinx.coroutines.*
fun main() = runBlocking {
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.")
}
结果如下
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.
可以看出线程取消后,后面逻辑就不再执行了
可取消的挂起函数会在取消时抛出CancellationException(但是这并不会有堆栈信息,因为系统认为取消四结束任务的标志,不需要暴漏出来),这可以用通常的方式处理。例如,当一个协程被取消时,try {...} finally {...}
表达式和 Kotlinuse
函数会正常执行它们的终结动作:
import kotlinx.coroutines.*
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.")
}
结果如下:
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.
有时候需要在协程的finally
中处理逻辑,但是在这里使用协程执行会出现错误。因为错误不会暴漏出来所以明面上只是不执行而已。比如以下代码
@Test
fun coroutinesCancelFinally(){
runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L)
}
} finally {
launch {
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
// job.cancel()
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.
期待的finally块中的代码并没有执行,这里需要使用withContext(NonCancellable)
来处理。所以修改为以下就可以了。需要注意的是该函数只能在这里使用
import kotlinx.coroutines.*
fun main() = runBlocking {
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.")
}
其结果如下
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.
如果我们执行某个协程,当它超过了某个我们期待的时间后,可以主动将其取消,这里需要使用withTimeout()
函数。
fun main() = runBlocking {
withTimeout(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
}
其结果如下
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
这里可以看到一个异常信息,这是因为超时被认为是一个需要用户知道的问题,如果不需要知道,可以使用withTimeoutOrNull
来替代
@Test
fun coroutinesTimeOut(){
runBlocking {
val result = withTimeoutOrNull(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
"Done" //假如任务没有超时可以返回一个结果,如果超时了会返回null,如果使用withTimeout则会抛出异常
}
println("Result is $result")
}
}
var acquired = 0
class Resource {
init { acquired++ } // Acquire the resource
fun close() { acquired-- } // Release the resource
}
这里需要注意的是withTimeout 中的超时事件与其块中运行的代码是异步的,并且可能随时发生,甚至在从超时块内部返回之前。如果您在块内打开或获取某些需要在块外关闭或释放的资源。
这里以一个例子作为解释(这里的例子其实并没有出现预想的问题,所以本能问题只作为记录)
@Test
fun coroutinesTimeOut2(){
runBlocking {
repeat(200_000) { // Launch 100K coroutines
launch(Dispatchers.IO) {
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
}
可以看到我们期望最终打印的值为0,但是多次运行后会发现偶尔打印出非0的值,可以使用以下方式修改
@Test
fun coroutinesTimeOut3(){
runBlocking {
repeat(100_000) { // Launch 100K 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
}
var acquired = 0
class Resource {
init { acquired++ } // Acquire the resource
fun close() { acquired-- } // Release the resource
}
协程的取消和超时
https://kotlinlang.org/docs/cancellation-and-timeouts.html