2、协程的取消及超时

这部分主要介绍协程的取消及超时(官方文档)

目录

1、取消协程

2、取消是协作式的

3、响应取消

4、使用finally关闭资源

5、运行不可取消的代码块(non-cancellable block)

6、超时

7、异步超时和资源


 

1、取消协程

在一个长时间运行的应用中你可能需要更细粒度的控制你的后台协程,比如一个用户在页面中启动了一个协程然后很快又把它关闭了,此时协程的返回结果已经不需要了,那么就应该在页面关闭的时候把它给取消掉;launch函数会返回一个job,你可以用这个job来取消协程的执行:

package com.cool.cleaner.test

import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() = runBlocking<Unit> {
    val job = launch {
        repeat(1000) { index ->
            println("job: I'm sleeping $index ...")
            delay(500)
        }
    }
    delay(1300)//等待一会,在超时之前不会继续往下执行
    println("main: I'm tired of waiting")
    job.cancel()
    job.join()
    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.

Process finished with exit code 0

当主函数中调用job.cancel的时候,因为协程已经取消就没有继续执行了;另外如果使用函数cancelAndJoin其实就相当于cancel和join的组合。

2、取消是协作式的

协程的取消是协作式的,协程必须作出配合才能取消。在kotlinx.coroutines库中所有的suspend函数都是可取消的,他们会检查协程的取消并在协程取消的时候抛出CancellationException;当然如果一个一个协程一直在计算并且没有检查是否已经取消,那么它就是不可取消的,就像下面下面的例子展示的一样:

package com.cool.cleaner.test

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) {
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500
            }
        }
    }
    delay(1300)//等待一会,在超时之前不会继续往下执行
    println("main: I'm tired of waiting")
    job.cancelAndJoin()
    println("main: Now I can quit.")
}


上面的例子将会一直执行并打印出"I'm sleeping" ,取消操作对它没有任何效果,协程将会在执行5次迭代后才执行完成。输出如下:

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.

Process finished with exit code 0

3、响应取消

有两种方法可以让计算型的代码(一直在做某种循环的代码)变得可取消。第一种方法是每隔一段时间调用一个suspend函数,suspend函数会检查取消操作,yield函数就是你的最好选择;另外一种方法就是显示的检查取消状态,这里学演示后一种方法。
把上面的循环条件while ( i < 5)替换为while (isActive),代码如下:

package com.cool.cleaner.test

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (isActive) {//让协程可取消
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500
            }
        }
    }
    delay(1300)//等待一会,在超时之前不会继续往下执行
    println("main: I'm tired of waiting")
    job.cancelAndJoin()
    println("main: Now I can quit.")
}

如你所见,现在循环是可取消的,isActive是CoroutineScope中可用的一个扩展属性。

4、使用finally关闭资源

可取消的suspend函数在取消的时候将会抛出 CancellationException异常,这个异常可以使用常规的方式处理,比如使用try {...} finally {...} 表达式,当协程取消的时候kotlin使用函数来运行finally中的代码块。如下代码所示:

package com.cool.cleaner.test

import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() = runBlocking<Unit> {
    val job = launch() {
        try {
            repeat(1000) { index ->
                println("job: I'm sleeping $index ...")
                delay(500)
            }
        } finally {
            println("job: I'm running finally")
        }
    }
    delay(1300)//等待一会,在超时之前不会继续往下执行
    println("main: I'm tired of waiting")
    job.cancelAndJoin()
    println("main: Now I can quit.")
}

join和cancelAndJoin都会等待finally代码块执行完成,上面的代码将会产生如下的输出:

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.

Process finished with exit code 0

5、运行不可取消的代码块(non-cancellable block)

在前面的例子中,如果你在finally代码块中使用suspend函数,那么将会产生CancellationException类型的异常(此处有疑问),这是因为运行这部分代码块的协程已经取消。通过情况下,这并不是一个值得注意的问题,因为所有行为良好的关闭动作(关闭文件、取消一个job或者关闭一些连接通道)都是非阻塞的而且也不会调用suspend函数。然而在极少的情况下你可能需要在取消的协程中挂起,此时你可以使用withContext(NonCancellable) {...}来封装你的代码,如下所示:

package com.cool.cleaner.test

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    val startTime = System.currentTimeMillis()
    val job = launch() {
        try {
            repeat(1000) { index ->
                println("job: I'm sleeping $index ...")
                delay(500)
            }
        } finally {
            withContext(NonCancellable) {
                println("job: I'm running finally")
                delay(1000)
                println("job: And I've just delayed for 1 sec because I'm non-cancellable")
            }
        }
    }
    delay(1300)//等待一会,在超时之前不会继续往下执行
    println("main: I'm tired of waiting")
    job.cancelAndJoin()
    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.

Process finished with exit code 0

上面标出的疑问,经过试验,把withContext这层包装去掉后,并没有看到抛出异常,估计是官网描述不准确;而是变成了下面的输出:

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.

Process finished with exit code 0

看到没,delay之后的代码都没有执行,所以建议是如果你想在finally里面执行一些操作的话最好是放在withContext(NonCancellable)里面。

6、超时

取消协程的一个最明显的原因是因为它的执行已经超时了;你可以持有协程返回的job引用,并在另一个协程中等待超时后再取消持有的job,不过有一个更简单的方法是使用内置的whithTimeout函数,如下代码所示:

package com.cool.cleaner.test

import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout

fun main() = runBlocking<Unit> {
    withTimeout(1300) {
        repeat(1000) { index ->
            println("job: I'm sleeping $index ...")
            delay(500)
        }
    }
}

将会产生如下输出:

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms
	at kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:186)
	at kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:156)
	at kotlinx.coroutines.EventLoopImplBase$DelayedRunnableTask.run(EventLoop.common.kt:497)
	at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
	at kotlinx.coroutines.DefaultExecutor.run(DefaultExecutor.kt:69)
	at java.lang.Thread.run(Thread.java:748)

Process finished with exit code 1

withTimeout抛出的异常TimeoutCancellationException 是CancellationException异常的子类,这里之所以没有看到异常栈是因为在已经取消的协程内部(很明显这里的描述也有问题,输出里面已经有异常栈了),CancellationException被当作是协程正常执行完成的一种情况。

这里需要补充说明下,如果withTimeout后面还有代码,那么后面的代码将会在withTimeout运行完成后才会执行,当然如果因为withTimeout超时而抛出异常了,那也不会执行了。比如下面的代码:

package com.cool.cleaner.test

import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout

fun main() = runBlocking<Unit> {
    withTimeout(1300) {
        repeat(10) { index ->
            println("job: I'm sleeping $index ...")
            delay(50)
        }
    }
    println("I'm Quite")
}

超时时间是1300 > 10 * 50,所以在还没超时前withTimeout里面的代码就执行完了,此时不会抛出异常,"I'm Quite"也会正常输出,其输出如下:

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
job: I'm sleeping 3 ...
job: I'm sleeping 4 ...
job: I'm sleeping 5 ...
job: I'm sleeping 6 ...
job: I'm sleeping 7 ...
job: I'm sleeping 8 ...
job: I'm sleeping 9 ...
I'm Quite

Process finished with exit code 0

回到正题,因为取消属于一种异常,所以所有的资源都需要正常关闭。如果你需要在协程超时的时候执行一些额外的操作,那么你可以将要执行的代码放在try {...} catch (e: TimeoutCancellationException) {...} 中,或者你可以使用函数withTimeoutOrNull ,此函数就像withTimeout一样,但是它会在超时的时候返回null而不是抛出异常。如下代码所示:

package com.cool.cleaner.test

import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull

fun main() = runBlocking<Unit> {
    val result = withTimeoutOrNull(1300) {
        repeat(1000) { index ->
            println("job: I'm sleeping $index ...")
            delay(500)
        }
        "Done"
    }
    println("Result is $result")
}

如果你运行上面的代码,你会发现它不会抛出异常,下面是运行结果:

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
Result is null

Process finished with exit code 0

可见,超时前返回了null,确实没抛出异常,也证实了只有withTimeout执行完成后,其后面的代码才会继续执行。

7、异步超时和资源

在使用withTimeout的时候,你应该意识到相对于withTimeout包装的代码来说它的超时是异步的,也就说可以在任何时候发生,甚至是在withTimeout代码块就要返回的时候; 你应该注意这个问题,特别是当你需要在withTimeout代码块中打开资源或者获取资源而需要在块外关闭或者释放资源的时候。
举个例子,这里我们使用Resource来代表一个可关闭的资源,当你需要资源的时候增加acquired,释放资源的时候调用close函数;这里我们运行很多协程,并在协程中使用withTimeout获取资源,在withTimeout外释放资源,代码如下:

package com.cool.cleaner.test

import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout

var acquired = 0
class Resource {
    init {
        acquired++//获取资源
    }

    fun close(): Unit {
        acquired--//释放资源
    }
}
fun main() {
    runBlocking {
        repeat(100_000) {
            launch {
                val resource = withTimeout(60) {
                    delay(50)
                    Resource()
                }
                resource.close()
            }
        }
    }
    println(acquired)
}

通过运行上面的代码你会发现并不是每次都会打印出0,尽管这可能取决于你的机器计时,但为了看到非0值你可能需要调整超时时间。

请注意,这里在10万个协程中增加和减少计数器是安全的,因为他们都运行在同一个线程。关于这个问题将会在下一篇的协程上下(coroutine context)文中讨论。

为了解决这个问题,你可以用一个变量持有资源的引用而不是从withTimeout代码块中返回它,如下代码所示:

package com.cool.cleaner.test

import kotlinx.coroutines.*

var acquired = 0
class Resource {
    init {
        acquired++//获取资源
    }

    fun close(): Unit {
        acquired--//释放资源
    }
}
fun main() {
    runBlocking {
        repeat(100_000) {//启动10万个协程
            launch {
                var resource: Resource? = null
                try {
                    withTimeout(60) {
                        delay(50)
                        resource = Resource()
                    }
                } catch (e: TimeoutCancellationException) {
//                    e.printStackTrace()
                } finally {
                    resource?.close()
                }
            }
        }
    }
    println(acquired)
}

注意,不用以为上面的代码超时时间是60而块中延时时间是50就不会超时了,实际上如果你把上面的注释打开的话就会看到一堆的超时。

上面的代码总会打印出0值,资源也没有泄漏的风险。
 


 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值