[Kotlin Tutorials 21] 协程的取消

这篇具有很好参考价值的文章主要介绍了[Kotlin Tutorials 21] 协程的取消。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

协程的取消

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

[Kotlin Tutorials 21] 协程的取消

本文属于合辑: 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是否依然活跃的例子:

fun main() = runBlocking {
    val startTime = currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (isActive) { // cancellable computation loop
            // print a message twice a second
            if (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.

catch Exception和runCatching

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

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

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

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

fun main() = runBlocking {
    val job = launch(Dispatchers.Default) {
        println("my long time function start")
        myLongTimeFunction()
        println("my other operations ==== ") // this line should not be printed when cancelled
    }
    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.")
}

private suspend fun myLongTimeFunction() = runCatching {
    var i = 0
    while (i < 10) {
        // print a message twice a second
        println("job: I'm sleeping ${i++} ...")
        delay(500)
    }
}

输出:

my long time function start
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
my other operations ==== 
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:

private suspend fun <T> suspendRunCatching(block: suspend () -> T): Result<T> = try {
    Result.success(block())
} catch (cancellationException: CancellationException) {
    throw cancellationException
} catch (exception: Exception) {
    Log.i(
        "suspendRunCatching",
        "Failed to evaluate a suspendRunCatchingBlock. Returning failure Result",
        exception
    )
    Result.failure(exception)
}

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

上面例子的输出变为:

my long time function start
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.

不想取消的处理

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

资源清理工作

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

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

例子:

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.")
}

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

需要更长生命周期的工作

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

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

再把这个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版本:

  • Cancellation and timeouts
  • Cancelling and timeouts github md version

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

  • Cancellation in coroutines
  • KotlinConf 2019: Coroutines! Gotta catch 'em all! by Florina Muntenescu & Manuel Vivo

其他:文章来源地址https://www.toymoban.com/news/detail-474975.html

  • Coroutines: first things first
  • Kotlin Coroutines and Flow - Use Cases on Android
  • Structured Concurrency Anniversary
  • Exceptions in coroutines
  • Coroutines & Patterns for work that shouldn’t be cancelled

到了这里,关于[Kotlin Tutorials 21] 协程的取消的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Kotlin: 协程的四种启动模式(CoroutineStart)

    点击查看CoroutineStart英文文档 创建协程的三种方式 runBlocking 运行一个协程并且会阻塞当前线程,直到它完成。 launch 启动一个新的协程,不会阻塞当前线程,并且返回一个Job,可以取消。 async async和await是两个函数,这两个函数在我们使用过程中一般都是成对出现的。 async用

    2024年04月23日
    浏览(50)
  • Unity 的协程的原理

    Unity是一款非常强大的游戏引擎,它支持多种编程语言,其中最常用的语言是C#。在Unity中,协程是一种非常强大的功能,它可以让我们在游戏中实现各种各样的效果。本文将详细介绍Unity协程的原理,并给出示例代码详解。 对啦!这里有个游戏开发交流小组里面聚集了一帮热

    2024年02月02日
    浏览(42)
  • golang 协程的实现原理

    要理解协程的实现, 首先需要了解go中的三个非常重要的概念, 它们分别是 G ,  M 和 P , 没有看过golang源代码的可能会对它们感到陌生, 这三项是协程最主要的组成部分, 它们在golang的源代码中无处不在. G (goroutine) G是goroutine的头文字, goroutine可以解释为受管理的轻量线程, gorout

    2024年02月10日
    浏览(44)
  • Unity中停止协程的多种方式解析

    在Unity3D游戏开发中,协程(Coroutine)是一种非常有用的功能,可以在游戏中实现延迟执行、定期执行和异步操作等任务。然而,有时候我们需要在运行时停止协程的执行。本文将介绍Unity中停止协程的几种常用方式,并提供相应的源代码示例。 使用StopCoroutine函数停止协程

    2024年02月03日
    浏览(70)
  • Android上的基于协程的存储框架

    在Android上,经常会需要持久化本地数据,比如我们需要缓存用户的配置信息、用户的数据、缓存数据、离线缓存数据等等。我们通常使用的工具为SharePreference、MMKV、DataStore、Room、文件等等。通过使用现有的存储框架,结合协程,我们可以方便地实现一个轻量级的响应式存储

    2024年02月13日
    浏览(35)
  • Unity 之 错误的停止协程的方式

    相信很多人都会这样开启一个协程 这样确实没啥毛病,那么怎么关掉这个协程呢,是不是在想也是一样的传cor_1()这个参数,然后start对应stop,试着输入stopCor....诶,代码提示有这个方法喔,然后写下了这样的代码 结果你会发现这个协程并没有被停下来。。。那该咋办呢?我在

    2024年02月16日
    浏览(38)
  • rust写一个多线程和协程的例子

    当涉及到多线程和协程时,Rust提供了一些非常强大的工具,其中最常用的库之一是 tokio ,它用于异步编程和协程。下面我将为你展示一个简单的Rust程序,演示如何使用多线程和协程。 首先,你需要在你的项目的 Cargo.toml 文件中添加 tokio 库的依赖: [dependencies] tokio = { versi

    2024年02月11日
    浏览(52)
  • 不同开发语言在进程、线程和协程的设计差异

    在多线程项目开发时,最常用、最常遇到的问题是 1,线程、协程安全 2,线程、协程间的通信和控制 本文主要探讨不同开发语言go、java、python在进程、线程和协程上的设计和开发方式的异同。 进程 进程是 操作系统进行资源分配的基本单位,每个进程都有自己的独立内存空

    2024年01月22日
    浏览(38)
  • 关于进程、线程、协程的概念以及Java中的应用

    本文将从“操作系统”、“Java应用”上两个角度来探究这三者的区别。 在我本人的疑惑中,我有以下3个问题。 在“多道程序环境下”,允许多个程序并发执行,此时它们将失去封闭性,并具有间断性以及不可再现性的特征,因此需要引入进程的概念。 进程是程序执行的过

    2024年02月08日
    浏览(62)
  • 使用C语言构建一个独立栈协程和共享栈协程的任务调度系统

    使用了标准库头文件 setjmp.h 中的 setjmp 和 longjmp 两个函数,构建了一个简单的查询式协作多任务系统,支持 独立栈 和 共享栈 两种任务。 其中涉及到获取和设置栈的地址操作,因此还需要根据不同平台提供获取和设置栈的地址操作(一般是汇编语言,因为涉及到寄存器) 该

    2024年02月19日
    浏览(51)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包