经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 移动开发 » Kotlin » 查看文章
[Kotlin Tutorials 21] 协程的取消
来源:cnblogs  作者:圣骑士wind  时间:2023/6/8 9:26:08  对本文有异议

协程的取消

本文讨论协程的取消, 以及实现时可能会碰到的几个问题.

coroutine cancellation

本文属于合辑: https://github.com/mengdd/KotlinTutorials

协程的取消

取消的意义: 避免资源浪费, 以及多余操作带来的问题.

基本特性:

  • cancel scope的时候会cancel其中的所有child coroutines.
  • 一旦取消一个scope, 你将不能再在其中launch新的coroutine.
  • 一个在取消状态的coroutine是不能suspend的.

如果一个coroutine抛出了异常, 它将会把这个exception向上抛给它的parent, 它的parent会做以下三件事情:

  • 取消其他所有的children.
  • 取消自己.
  • 把exception继续向上传递.

Android开发中的取消

在Android开发中, 比较常见的情形是由于View生命周期的终止, 我们需要取消一些操作.

通常我们不需要手动调用cancel()方法, 那是因为我们利用了一些更高级的包装方法, 比如:

  • viewModelScope: 会在ViewModel onClear的时候cancel.
  • lifecycleScope: 会在作为Lifecycle Owner的View对象: Activity, Fragment到达DESTROYED状态时cancel.

取消并不是自动获得的

all suspend functions from kotlinx.coroutines are cancellable, but not yours.

kotlin官方提供的suspend方法都会有cancel的处理, 但是我们自己写的suspend方法就需要自己留意.
尤其是耗时或者带循环的地方, 通常需要自己加入检查, 否则即便调用了cancel, 代码也继续在执行.

有这么几种方法:

  • isActive()
  • ensureActive()
  • yield(): 除了ensureActive以外, 会出让资源, 比如其他工作不需要再往线程池里加线程.

一个在循环中检查coroutine是否依然活跃的例子:

  1. fun main() = runBlocking {
  2. val startTime = currentTimeMillis()
  3. val job = launch(Dispatchers.Default) {
  4. var nextPrintTime = startTime
  5. var i = 0
  6. while (isActive) { // cancellable computation loop
  7. // print a message twice a second
  8. if (currentTimeMillis() >= nextPrintTime) {
  9. println("job: I'm sleeping ${i++} ...")
  10. nextPrintTime += 500L
  11. }
  12. }
  13. }
  14. delay(1300L) // delay a bit
  15. println("main: I'm tired of waiting!")
  16. job.cancelAndJoin() // cancels the job and waits for its completion
  17. println("main: Now I can quit.")
  18. }

输出:

  1. job: I'm sleeping 0 ...
  2. job: I'm sleeping 1 ...
  3. job: I'm sleeping 2 ...
  4. main: I'm tired of waiting!
  5. main: Now I can quit.

catch Exception和runCatching

众所周知catch一个很general的Exception类型可能不是一个好做法.
因为你以为捕获了A, B, C异常, 结果实际上还有D, E, F.

捕获具体的异常类型, 在开发阶段的快速失败会帮助我们更早定位和解决问题.

协程还推出了一个"方便"的runCatching方法, catchThrowable.
让我们写出了看似更"保险", 但却更容易破坏取消机制的代码.

如果我们catch了CancellationException, 会破坏Structured Concurrency.
看这个例子:

  1. fun main() = runBlocking {
  2. val job = launch(Dispatchers.Default) {
  3. println("my long time function start")
  4. myLongTimeFunction()
  5. println("my other operations ==== ") // this line should not be printed when cancelled
  6. }
  7. delay(1300L) // delay a bit
  8. println("main: I'm tired of waiting!")
  9. job.cancelAndJoin() // cancels the job and waits for its completion
  10. println("main: Now I can quit.")
  11. }
  12. private suspend fun myLongTimeFunction() = runCatching {
  13. var i = 0
  14. while (i < 10) {
  15. // print a message twice a second
  16. println("job: I'm sleeping ${i++} ...")
  17. delay(500)
  18. }
  19. }

输出:

  1. my long time function start
  2. job: I'm sleeping 0 ...
  3. job: I'm sleeping 1 ...
  4. job: I'm sleeping 2 ...
  5. main: I'm tired of waiting!
  6. my other operations ====
  7. main: Now I can quit.

当job cancel了以后后续的工作不应该继续进行, 然而我们可以看到log仍然被打印出来, 这是因为runCatching把异常全都catch了.

这里有个open issue讨论这个问题: https://github.com/Kotlin/kotlinx.coroutines/issues/1814

CancellationException的特殊处理

如何解决上面的问题呢? 基本方案是把CancellationException再throw出来.

比如对于runCatching的改造, NowInAndroid里有这么一个方法suspendRunCatching:

  1. private suspend fun <T> suspendRunCatching(block: suspend () -> T): Result<T> = try {
  2. Result.success(block())
  3. } catch (cancellationException: CancellationException) {
  4. throw cancellationException
  5. } catch (exception: Exception) {
  6. Log.i(
  7. "suspendRunCatching",
  8. "Failed to evaluate a suspendRunCatchingBlock. Returning failure Result",
  9. exception
  10. )
  11. Result.failure(exception)
  12. }

上面的例子改为用这个suspendRunCatching方法替代runCatching就修好了.

上面例子的输出变为:

  1. my long time function start
  2. job: I'm sleeping 0 ...
  3. job: I'm sleeping 1 ...
  4. job: I'm sleeping 2 ...
  5. main: I'm tired of waiting!
  6. main: Now I can quit.

不想取消的处理

可能还有一些工作我们不想随着job的取消而完全取消.

资源清理工作

finally通常用于try block之后的的资源清理, 如果其中没有suspend方法那么没有问题.

如果finally中的代码是suspend的, 如前所述, 一个在取消状态的coroutine是不能suspend的.
那么需要用一个withContext(NonCancellable).

例子:

  1. fun main() = runBlocking {
  2. val job = launch {
  3. try {
  4. repeat(1000) { i ->
  5. println("job: I'm sleeping $i ...")
  6. delay(500L)
  7. }
  8. } finally {
  9. withContext(NonCancellable) {
  10. println("job: I'm running finally")
  11. delay(1000L)
  12. println("job: And I've just delayed for 1 sec because I'm non-cancellable")
  13. }
  14. }
  15. }
  16. delay(1300L) // delay a bit
  17. println("main: I'm tired of waiting!")
  18. job.cancelAndJoin() // cancels the job and waits for its completion
  19. println("main: Now I can quit.")
  20. }

注意这个方法一般用于会suspend的资源清理, 不建议在各个场合到处使用, 因为它破坏了对coroutine执行取消的控制.

需要更长生命周期的工作

如果有一些工作需要比View/ViewModel更长的生命周期, 可以把它放在更下层, 用一个生命周期更长的scope.
可以根据不同的场景设计, 比如可以用一个application生命周期的scope:

  1. class MyApplication : Application() {
  2. // No need to cancel this scope as it'll be torn down with the process
  3. val applicationScope = CoroutineScope(SupervisorJob() + otherConfig)
  4. }

再把这个scope注入到repository中去.

如果需要做的工作比application的生命周期更长, 那么可以考虑用WorkManager.

总结: 不要破坏Structured Concurrency

Structure Concurrency为开发者提供了方便管理多个coroutines的有效方法.
基本上破坏Structure Concurrency特性的行为(比如用GlobalScope, 用NonCancellable, catch CancellationException等)都是反模式, 要小心使用.

还要注意不要随便传递job.
CoroutineContext有一个元素是job, 但是这并不意味着我们可以像切Dispatcher一样随便传一个job参数进去.
文章: Structured Concurrency Anniversary

看这里: https://github.com/Kotlin/kotlinx.coroutines/issues/1001

References & Further Reading

Kotlin官方文档的网页版和markdown版本:

Android官方文档上链接的博客和视频:

其他:

原文链接:https://www.cnblogs.com/mengdd/p/kotlin-coroutine-cancellation.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号