[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版本:
Android官方文档上链接的博客和视频:
- Cancellation in coroutines
- KotlinConf 2019: Coroutines! Gotta catch 'em all! by Florina Muntenescu & Manuel Vivo
其他:
- 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] 协程的取消的更多相关文章
- kotlin学习-Coroutines(协程)
协程(又名纤程),轻量级线程(建立在线程基础上,属于用户态调用),非阻塞式编程(像同步编写一样),在用户态内进行任务调度,避免与内核态过多交互问题,提高程序快速响应.协程使用挂起当前上下文替代阻塞,被 ...
- Kotlin Coroutine(协程): 二、初识协程
@ 目录 前言 一.初识协程 1.runBlocking: 阻塞协程 2.launch: 创建协程 3.Job 4.coroutineScope 5.协程取消 6.协程超时 7.async 并行任务 ...
- Kotlin Coroutine(协程): 三、了解协程
@ 目录 前言 一.协程上下文 1.调度器 2.给协程起名 3.局部变量 二.启动模式 CoroutineStart 三.异常处理 1.异常测试 2.CoroutineExceptionHandler ...
- Kotlin 协程一 —— 全面了解 Kotlin 协程
一.协程的一些前置知识 1.1 进程和线程 1.1.1基本定义 1.1.2为什么要有线程 1.1.3 进程与线程的区别 1.2 协作式与抢占式 1.2.1 协作式 1.2.2 抢占式 1.3 协程 二 ...
- Kotlin协程解析系列(上):协程调度与挂起
vivo 互联网客户端团队- Ruan Wen 本文是Kotlin协程解析系列文章的开篇,主要介绍Kotlin协程的创建.协程调度与协程挂起相关的内容 一.协程引入 Kotlin 中引入 Corout ...
- Kotlin协程第一个示例剖析及Kotlin线程使用技巧
Kotlin协程第一个示例剖析: 上一次https://www.cnblogs.com/webor2006/p/11712521.html已经对Kotlin中的协程有了理论化的了解了,这次则用代码来直 ...
- Kotlin协程入门
开发环境 IntelliJ IDEA 2021.2.2 (Community Edition) Kotlin: 212-1.5.10-release-IJ5284.40 介绍Kotlin中的协程.用一 ...
- Android中的Coroutine协程原理详解
前言 协程是一个并发方案.也是一种思想. 传统意义上的协程是单线程的,面对io密集型任务他的内存消耗更少,进而效率高.但是面对计算密集型的任务不如多线程并行运算效率高. 不同的语言对于协程都有不同的实 ...
- Unity协程Coroutine使用总结和一些坑
原文摘自 Unity协程Coroutine使用总结和一些坑 MonoBehavior关于协程提供了下面几个接口: 可以使用函数或者函数名字符串来启动一个协程,同时可以用函数,函数名字符串,和Corou ...
- 第二章 - Java与协程
Java与协程 内核线程的局限 通过一个具体场景来解释目前Java线程面临的困境.今天对Web应用的服务要求,不论是在请求数量上还是在复杂度上,与十多年前相比已不可同日而语,这一方面是源于业务量的增长 ...
随机推荐
- [C++STL教程]7.priority_queue优先队列入门学习!零基础都能听懂的教程
不知不觉C++STL教程系列已经第7期了.之前我们介绍过:vector, queue, stack, set, map等等数据结构. 今天我们来学习一个新的stl容器:priority_queue优先 ...
- 设计模式-用代理模式(Proxy Pattern)来拯救你的代码:打造可靠的程序设计
前言 设计模式是一种高级编程技巧,也是一种通用的解决方案.它能在不同的应用场景中使用,它可以提高代码的可读性.可复用性和可维护性.设计模式的学习能提高我们的编程能力以及代码质量,同时也能提高我们的开发 ...
- spark中的持久化机制以及lineage和checkpoint(简含源码解析)
spark相比MapReduce最大的优势是,spark是基于内存的计算模型,有的spark应用比较复杂,如果中间出错了,那么只能根据lineage从头开始计算,所以为了避免这种情况,spark提供了 ...
- vulnhub靶场之ORASI: 1
准备: 攻击机:虚拟机kali.本机win10. 靶机:Orasi: 1,下载地址:https://download.vulnhub.com/orasi/Orasi.ova,下载后直接vbox打开即可 ...
- 面试某大厂,被Channel给吊打了,这次一次性通关channel!
目录 一 前言 面试题 然后我们进行一下扩展,玩转Channel! 二 解决面试题 1. 介绍一下Channel 2. Channel在go中起什么作用 3. Channel为什么需要两个队列实现 4 ...
- 12-提取css成单独文件
const { resolve } = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') const M ...
- c# 异步进阶———— 自定义 taskschedule[三]
前言 我们知道我们的task async 和 await 是基于线程池进行调度的. 但是async 和 await 也就是使用了默认的task调度,让其在线程池中运行. 但是线程池是榨干机器性能为本质 ...
- 深度学习--实战 LeNet5
深度学习--实战 LeNet5 数据集 数据集选用CIFAR-10的数据集,Cifar-10 是由 Hinton 的学生 Alex Krizhevsky.Ilya Sutskever 收集的一个用于普 ...
- Win Pycharm + Appium + 夜神模拟器 实现APP自动化
前言: 之前的文章已经介绍完通过使用 真机 进行APP自动化.此篇文章将介绍使用 夜神模拟器(Nox) 进行APP自动化测试. 一.基础配置 1.请移步此篇文章(https://www.cnblogs ...
- 通过Handsontable实现像Excel一样编辑数据
一.Handsontable是指什么? 官网: http://handsontable.com Handsontable是一个JavaScript库,可以帮助您轻松实现类似Excel电子表格一样的编 ...