协程的取消

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

本文属于合辑: 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版本:

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

其他:

[Kotlin Tutorials 21] 协程的取消的更多相关文章

  1. kotlin学习-Coroutines(协程)

    协程(又名纤程),轻量级线程(建立在线程基础上,属于用户态调用),非阻塞式编程(像同步编写一样),在用户态内进行任务调度,避免与内核态过多交互问题,提高程序快速响应.协程使用挂起当前上下文替代阻塞,被 ...

  2. Kotlin Coroutine(协程): 二、初识协程

    @ 目录 前言 一.初识协程 1.runBlocking: 阻塞协程 2.launch: 创建协程 3.Job 4.coroutineScope 5.协程取消 6.协程超时 7.async 并行任务 ...

  3. Kotlin Coroutine(协程): 三、了解协程

    @ 目录 前言 一.协程上下文 1.调度器 2.给协程起名 3.局部变量 二.启动模式 CoroutineStart 三.异常处理 1.异常测试 2.CoroutineExceptionHandler ...

  4. Kotlin 协程一 —— 全面了解 Kotlin 协程

    一.协程的一些前置知识 1.1 进程和线程 1.1.1基本定义 1.1.2为什么要有线程 1.1.3 进程与线程的区别 1.2 协作式与抢占式 1.2.1 协作式 1.2.2 抢占式 1.3 协程 二 ...

  5. Kotlin协程解析系列(上):协程调度与挂起

    vivo 互联网客户端团队- Ruan Wen 本文是Kotlin协程解析系列文章的开篇,主要介绍Kotlin协程的创建.协程调度与协程挂起相关的内容 一.协程引入 Kotlin 中引入 Corout ...

  6. Kotlin协程第一个示例剖析及Kotlin线程使用技巧

    Kotlin协程第一个示例剖析: 上一次https://www.cnblogs.com/webor2006/p/11712521.html已经对Kotlin中的协程有了理论化的了解了,这次则用代码来直 ...

  7. Kotlin协程入门

    开发环境 IntelliJ IDEA 2021.2.2 (Community Edition) Kotlin: 212-1.5.10-release-IJ5284.40 介绍Kotlin中的协程.用一 ...

  8. Android中的Coroutine协程原理详解

    前言 协程是一个并发方案.也是一种思想. 传统意义上的协程是单线程的,面对io密集型任务他的内存消耗更少,进而效率高.但是面对计算密集型的任务不如多线程并行运算效率高. 不同的语言对于协程都有不同的实 ...

  9. Unity协程Coroutine使用总结和一些坑

    原文摘自 Unity协程Coroutine使用总结和一些坑 MonoBehavior关于协程提供了下面几个接口: 可以使用函数或者函数名字符串来启动一个协程,同时可以用函数,函数名字符串,和Corou ...

  10. 第二章 - Java与协程

    Java与协程 内核线程的局限 通过一个具体场景来解释目前Java线程面临的困境.今天对Web应用的服务要求,不论是在请求数量上还是在复杂度上,与十多年前相比已不可同日而语,这一方面是源于业务量的增长 ...

随机推荐

  1. 华为Sound Joy用后感

    在买华为Sound Joy音响前,我就在几个相似的音响之中衡量,其中有MIFA WildRod和JBL 万花筒6做了对比,在经过一系列的对比(网上查阅资料)之后,我最终选择了华为的Sound Joy这 ...

  2. 《HelloTester》第3期

    1.前言 读了之前的简历篇和投递篇,你应该开始准备迎接面试了,那么,在面试前,我们都需要准备些什么呢?是要默默的去背诵那些理论知识,还是熟悉一下自己的技能?又或者去看看公司要求的那些技术呢?接下来这篇 ...

  3. 可视化—AntV G6 紧凑树实现节点与边动态样式、超过X条展示更多等实用小功能

    通过一段时间的使用和学习,对G6有了更一步的经验,这篇博文主要从以下几个小功能着手介绍,文章最后会给出完整的demo代码. 目录 1. 树图的基本布局和使用 2. 根据返回数据的属性不同,定制不一样的 ...

  4. 二进制安装Kubernetes(k8s) v1.23.4

    1.环境 网段 物理主机:192.168.1.0/24 service:10.96.0.0/12 pod:172.16.0.0/12 如果有条件建议k8s集群与etcd集群分开安装 1.1.k8s基础 ...

  5. 四月十八日java基础知识

    1.由于每个对象的pi值都是相同的,所以没有必要让每个对象都保存有自己的pi值,因此将pi声明为静态变量,使之成为所有对象共用的存储空间,所有对象都公用pi这个变量也就是说共用的变量可以设定为静态变量 ...

  6. LeeCode 栈与队列问题(二)

    LeeCode 239: 滑动窗口最大值 题目描述 给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧.你只可以看到在滑动窗口内的 k 个数字.滑动窗口每次只向右 ...

  7. 人群定向SQL表

    SET FOREIGN_KEY_CHECKS=0; -- ---------------------------- -- Table structure for rc_throng -- ------ ...

  8. 如何在模型中引入可学习参数(Pytorch)

    错误实例: def init(self): self.w1 = torch.nn.Parameter(torch.FloatTensor(1),requires_grad=True).cuda() s ...

  9. Pytorch实践模型训练(损失函数)

    一个深度学习项目包括了: 模型设计.损失函数设计.梯度更新方法.模型保存和加载和模型训练,其中损失函数就像一把衡量模型学习效果的尺子,训练模型的过程就是优化损失函数的过程,模型拿到数据之后有一个非常重 ...

  10. 17-js代码压缩

    const { resolve } = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); modul ...