Kotlin协程详解——协程取消与超时

目录

一、协程取消

1.取消协程的执行

2.使计算代码可取消

3.在finally中释放资源

4.运行不能取消的代码块

二、协程超时

异步超时与资源管理


一、协程取消

1.取消协程的执行

在一个长时间运行的应用程序中,你也许需要对你的后台协程进行细粒度的控制。 比如说,
一个用户也许关闭了一个启动了协程的界面,那么现在协程的执行结果已经不再被需要了,
这时,它应该是可以被取消的。 该 launch 函数返回了一个可以被用来取消运行中的协程的
Job:

 runBlocking {val job = launch {Log.d(TAG,"job: I'm sleeping ...")}delay(100L)Log.d(TAG,"main: I'm tired of waiting!")job.cancel() // 取消该作业job.join() // 等待作业执行结束Log.d(TAG,"main: Now I can quit.")}

一旦 main 函数调用了 job.cancel ,我们在其它的协程中就看不到任何输出,因为它被取消
了。 这里也有一个可以使 Job 挂起的函数 cancelAndJoin 它合并了对 cancel 以及 join 的调用。

2.取消是协作的

协程的取消是协作的。一段协程代码必须协作才能被取消。 所有 kotlinx.coroutines 中的
挂起函数都是 可被取消的 。它们检查协程的取消, 并在取消时抛出 CancellationException。
然而,如果协程正在执行计算任务,并且没有检查取消的话,那么它是不能被取消的,就如
如下示例代码所示:

runBlocking {
//sampleStart
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU
// 每秒打印消息两次
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // 等待一段时间
println("main: I'm tired of waiting!")
job.cancelAndJoin() // 取消一个作业并且等待它结束
println("main: Now I can quit.")
//sampleEnd
}

运行示例代码,并且我们可以看到它连续打印出了“I'm sleeping”,甚至在调用取消后, 作业
仍然执行了五次循环迭代并运行到了它结束为止。

可以通过捕获CancellationException而不重新抛出它来观察到相同的问题。

 runBlocking {//sampleStartval job = launch(Dispatchers.Default) {repeat(5) { i ->try {Log.d(TAG,"job: I'm sleeping $i ...")delay(500)} catch (e: CancellationException) {Log.d(TAG,"CancellationException")Log.d(TAG,e.toString())}}}delay(1300L) // delay a bitLog.d(TAG,"main: I'm tired of waiting!")job.cancelAndJoin() // cancels the job and waits for its completionLog.d(TAG,"main: Now I can quit.")}

虽然捕获异常(Exception)被视为一种反模式(anti-pattern),但这个问题可能会以更微妙的方式出现,比如在使用runCatching函数时,该函数不会重新抛出CancellationException

为什么取消协程是协作的?

在编程中,特别是在涉及并发和异步操作的上下文中,"协程"(Coroutine)是一种可以暂停和恢复执行的函数或方法。协程允许程序在等待某些操作(如I/O操作)完成时释放执行权,从而允许其他任务运行,这提高了程序的效率和响应性。

当我们说“一段协程代码必须协作才能被取消”时,意味着协程本身需要包含一些机制或代码,以响应外部的取消请求。这不同于传统的线程或进程,后者可以通过操作系统层面的信号或中断直接强制终止。协程的取消需要更加细致和协作的处理,因为协程的运行是在用户态管理的,而不是由操作系统直接控制。

以下是一些关键点,解释了为什么协程的取消需要协作:

  1. 状态管理:协程可能处于多种状态(如运行中、等待中、已完成等)。要安全地取消协程,需要确保它不会在取消过程中处于不一致的状态。

  2. 资源清理:协程可能会分配资源(如内存、文件句柄、网络连接等)。取消协程时,需要确保这些资源被适当地释放或回收,以避免资源泄漏。

  3. 取消点:协程需要在其执行路径上明确设置“取消点”,这些点是检查取消请求的地方。如果在这些点检测到取消请求,协程将停止执行并适当地清理资源。

  4. 异常处理:取消协程通常通过抛出或传播异常来实现。这意味着协程需要能够捕获并处理这些异常,以避免程序崩溃。

  5. 用户态控制:由于协程的调度和执行是在用户态进行的,没有操作系统的直接干预,因此取消操作需要协程代码本身的支持和配合。

总之,协程的取消机制依赖于协程内部的协作,这意味着协程需要编写成能够响应取消请求的形式,包括在适当的时候检查取消状态、清理资源、以及适当地处理取消操作引发的异常。这种设计使得协程的取消更加灵活和安全,但同时也要求开发者在编写协程时更加注意取消逻辑的实现。

2.使计算代码可取消

我们有两种方法来使执行计算的代码可以被取消。第一种方法是定期调用挂起函数来检查取
消。对于这种目的 yield 是一个好的选择。 另一种方法是显式的检查取消状态。让我们试试第
二种方法。
将前一个示例中的 while (i < 5) 替换为 while (isActive) 并重新运行它。

runBlocking {//sampleStartval startTime = System.currentTimeMillis()val job = launch(Dispatchers.Default) {var nextPrintTime = startTimevar i = 0while (isActive) { // 可以被取消的计算循环if (System.currentTimeMillis() >= nextPrintTime) {println("job: I'm sleeping ${i++} ...")nextPrintTime += 500L}}}delay(1300L) // 等待一段时间println("main: I'm tired of waiting!")job.cancelAndJoin() // 取消该作业并等待它结束println("main: Now I can quit.")}

你可以看到,现在循环被取消了。isActive 是一个可以被使用在 CoroutineScope 中的扩展属
性。

3.在finally中释放资源

我们通常使用如下的方法处理在被取消时抛出 CancellationException 的可被取消的挂起函数。
比如说, try {……} finally {……} 表达式以及 Kotlin 的 use 函数一般在协程被取消的时候执
行它们的终结动作:

 runBlocking {//sampleStartval job = launch {try {repeat(1000) { i ->println("job: I'm sleeping $i ...")delay(500L)}} catch (e:CancellationException){println("CancellationException:"+e.message)} finally {println("job: I'm running finally")}}delay(1300L) // 延迟一段时间println("main: I'm tired of waiting!")job.cancelAndJoin() // 取消该作业并且等待它结束println("main: Now I can quit.")}

join 和 cancelAndJoin 等待了所有的终结动作执行完毕, 所以运行示例得到了下面的输出:
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.

4.运行不能取消的代码块

在前一个例子中任何尝试在 finally 块中调用挂起函数的行为都会抛出
CancellationException,因为这里持续运行的代码是可以被取消的。通常,这并不是一个问
题,所有良好的关闭操作(关闭一个文件、取消一个作业、或是关闭任何一种通信通道)通
常都是非阻塞的,并且不会调用任何挂起函数。然而,在真实的案例中,当你需要挂起一个
被取消的协程,你可以将相应的代码包装在 withContext(NonCancellable) {……} 中,并使用
withContext 函数以及 NonCancellable 上下文,见如下示例所示:

runBlocking {//sampleStartval 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)//如果不用withContext,则此句话不会被打印println("job: And I've just delayed for 1 sec because I'm non-cancella ble")}}}delay(1300L) // 延迟一段时间println("main: I'm tired of waiting!")job.cancelAndJoin() // 取消该作业并等待它结束println("main: Now I can quit.")}

关于 Kotlin 协程(Coroutines)中处理取消操作和异常情况的说明:

  1. 协程和取消操作:在 Kotlin 协程中,协程可以被取消以释放资源或响应超时等情况。当协程被取消时,它会尽可能快地抛出一个 CancellationException 异常来通知调用者或相关代码,当前操作已被取消。

  2. finally 块中的挂起函数:在 try-catch-finally 结构中,finally 块用于执行清理操作,无论 try 块中的代码是否成功执行或抛出异常。如果在 finally 块中调用了挂起函数(即一个可能挂起执行的函数,如网络请求或延迟操作),并且此时协程已被取消,那么尝试执行这个挂起函数将会导致 CancellationException 被抛出。

  3. 为什么通常不是问题:通常,finally 块中的清理操作(如关闭文件、取消作业、关闭通信通道等)都是非阻塞的,意味着它们不需要等待其他操作完成。因此,这些操作通常不会调用挂起函数,从而避免了在协程被取消时抛出 CancellationException 的问题。

  4. 处理被取消的协程中的挂起函数:然而,在某些情况下,你可能需要在已被取消的协程中执行挂起函数。例如,你可能需要在取消操作时执行一些清理工作,这些工作本身包含挂起操作。为了在这种情况下避免 CancellationException,Kotlin 协程提供了 withContext(NonCancellable) { ... } 结构。

  5. withContext(NonCancellable) { ... }:这个函数允许你在一个不会被取消的上下文中执行代码块。这意味着,即使外部协程已被取消,withContext(NonCancellable) { ... } 内部的代码仍然会执行,而不会抛出 CancellationException。这对于执行必须完成的清理操作特别有用,即使这些操作包含挂起函数。

综上所述,这段话是在说明如何在 Kotlin 协程中处理取消操作和异常,特别是在需要执行挂起函数作为清理操作时,如何使用 withContext(NonCancellable) { ... } 来确保这些操作能够安全执行,而不会因协程被取消而失败。

二、协程超时

在实践中绝大多数取消一个协程的理由是它有可能超时。 当你手动追踪一个相关 Job 的引用
并启动了一个单独的协程在延迟后取消追踪,这里已经准备好使用 withTimeout 函数来做这件
事。 来看看示例代码:

runBlocking {//sampleStarttry {var result = withTimeout(4000L){repeat(1000){i->println("I am sleep $i...")delay(500L)}}Log.d(TAG,"result:"+result.toString())}catch (e:TimeoutCancellationException){Log.d(TAG,"TimeoutCancellationException:"+e.message)}}

withTimeout 抛出了 TimeoutCancellationException ,它是 CancellationException 的子类。 我
们之前没有在控制台上看到堆栈跟踪信息的打印。这是因为在被取消的协程中
CancellationException 被认为是协程执行结束的正常原因。 然而,在这个示例中我们在
main 函数中正确地使用了 withTimeout 。
由于取消只是一个例外,所有的资源都使用常用的方法来关闭。 如果你需要做一些各类使用
超时的特别的额外操作,可以使用类似 withTimeout 的 withTimeoutOrNull 函数,并把这些会
超时的代码包装在 try {...} catch (e: TimeoutCancellationException) {...} 代码块中,而
withTimeoutOrNull 通过返回 null 来进行超时操作,从而替代抛出一个异常:

 runBlocking {//sampleStarttry {var result = withTimeoutOrNull(4000L){repeat(1000){i->println("I am sleep $i...")delay(500L)}}Log.d(TAG,"result:"+result.toString())}catch (e:TimeoutCancellationException){Log.d(TAG,"TimeoutCancellationException:"+e.message)}}

运行这段代码时不再抛出异常,输出为:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Result is null

异步超时与资源管理

withTimeout中的超时事件相对于其代码块内运行的代码是异步的,并且可能随时发生,甚至在从超时块内部返回之前立即发生。如果你在该代码块内部打开或获取了一些需要在代码块外部关闭或释放的资源,请牢记这一点。

例如,这里我们使用Resource类来模拟一个可关闭的资源,该类仅通过递增获取计数器并在其close函数中递减计数器来跟踪它被创建了多少次。现在,让我们创建大量协程,每个协程在withTimeout块的末尾创建一个Resource并在块外部释放该资源。我们添加了一个小的延迟,以便更有可能在withTimeout块刚好完成时发生超时,这将导致资源泄漏。

这段话是关于Kotlin协程中withTimeout函数行为的一个重要说明。让我们一步步解析它的含义:

  1. withTimeout中的超时事件是异步的:这意味着超时不是由withTimeout代码块内部的代码直接控制的。它不是在该代码块执行到某个特定点时触发的,而是由协程调度器根据指定的超时时间来管理的。因此,即使代码块内部的代码正在运行,超时也可能在任何时候发生。

  2. 可能随时发生,甚至在从超时块内部返回之前立即发生:这句话进一步强调了超时的异步性。它表明,即使代码块内部的代码看起来即将完成并准备返回,超时也可能在返回操作实际发生之前的一瞬间触发。这种情况可能导致一些棘手的问题,特别是当涉及到资源管理时。

  3. 如果你在代码块内部打开或获取了一些资源:这里提到的“资源”可以是任何需要在使用完毕后关闭或释放的东西,比如文件句柄、数据库连接、网络连接等。在withTimeout代码块内部打开或获取这些资源是常见的做法,但你需要意识到超时的异步性可能导致这些资源在未被正确关闭或释放的情况下被遗弃。

  4. 请牢记这一点:这是一个警告,提醒开发者在使用withTimeout时需要特别注意资源管理。由于超时的异步性,你不能简单地假设代码块内部的代码总是会执行到末尾并有机会关闭或释放所有资源。因此,你需要采取额外的措施来确保资源的正确管理,比如使用try-finally结构来确保资源在发生超时或其他异常时也能被正确关闭或释放。

综上所述,这段话是在强调在使用withTimeout时需要注意资源管理的异步性和潜在的风险,以及需要采取适当的措施来确保资源的正确管理。

  runBlocking {//sampleStartrepeat(10000){launch {var res = withTimeout(60){delay(50)Resource()}res.close()}}}Log.d(TAG,"aq:$aq")class Resource{var aq = 0init{aq++}fun close(){aq--}}

如果你运行上面的代码,你会发现它并不总是打印零,尽管这可能会取决于你机器的时序。你可能需要调整这个示例中的超时时间,以便实际看到非零值。

请注意,这里从10K个协程中对已获取的计数器进行增减操作是完全线程安全的,因为这一操作总是在同一个线程中执行,即runBlocking所使用的线程。关于这一点的更多信息,将在关于协程上下文的章节中解释。

为了解决这个问题,你可以将资源的引用存储在一个变量中,而不是从withTimeout块中返回它。

runBlocking {//sampleStartrepeat(10000){launch {var res: Resource? = nulltry {res = withTimeout(60){delay(50)Resource()}}finally {res?.close()}}}}Log.d(TAG,"aq:$aq")class Resource{var aq = 0init{aq++}fun close(){aq--}
}

输出结果一直为0,Resource没有泄露。

推荐文章

取消与超时 · Kotlin 官方文档 中文版

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/14658.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java版本与JDK版本

两者关联 Java版本指的Java语言和平台的版本&#xff0c;例如Java8、Java11、Java17等&#xff0c;每个版本会引入新特性、改进和修复。 JDK(Java Development Kit)版本则是开发工具包&#xff0c;包含编译器、调试器等工具&#xff0c;通常与Java版本对应&#xff0c;例如JDK…

【C语言标准库函数】三角函数

目录 一、头文件 二、函数简介 2.1. 正弦函数&#xff1a;sin(double angle) 2.2. 余弦函数&#xff1a;cos(double angle) 2.3. 正切函数&#xff1a;tan(double angle) 2.4. 反正弦函数&#xff1a;asin(double value) 2.5. 反余弦函数&#xff1a;acos(double value)…

活动预告 |【Part 2】Microsoft 安全在线技术公开课:通过扩展检测和响应抵御威胁

课程介绍 通过 Microsoft Learn 免费参加 Microsoft 安全在线技术公开课&#xff0c;掌握创造新机遇所需的技能&#xff0c;加快对 Microsoft Cloud 技术的了解。参加我们举办的“通过扩展检测和响应抵御威胁”技术公开课活动&#xff0c;了解如何更好地在 Microsoft 365 Defen…

MySQL第五次作业

根据图片内容完成作业 1.建表 &#xff08;1&#xff09;建立两个表:goods(商品表)、orders(订单表) mysql> create table goods( -> gid char(8) primary key, -> name varchar(10), -> price decimal(8,2), -> num int); mysql> create t…

Breakout靶场小试牛刀

1.首先经典两件套 nmap -A 扫描 发现开放很多端口&#xff08;80&#xff0c;10000&#xff0c;20000为重点关注&#xff09; 问题不大&#xff0c;先dirsearch扫一下目录再说 结果能看的manual里啥也没有&#xff0c;之后再查看80端口界面源代码 发现有一串字符 但是问了ai…

Vue el-tree 加载过滤出的父节点以及其包含的子节点

由于el-tree提供的过滤函数&#xff0c;过滤出来的目录节点不包含子节点&#xff0c;因此需要改造过滤函数&#xff0c;使其过滤出的目录节点包含子节点。 <template><div><el-input placeholder"请输入内容" v-model"filterText" clearab…

认识O(NlogN)的排序

归并排序 归并排序&#xff08;任何一个递归&#xff09;如果不懂可以画一个树状结构去帮助自己去理解。 核心排序方法为Merger public class 归并排序 {public static void main(String[] args) {int[] arr1 {3, 1, 2, 2, 5, 6};int[] arr2 Arrays.copyOf(arr1, arr1.len…

如何使用Gemini模型,国内如何订阅购买Gemini Pro的教程,Gemini Pro 免费试用操作步骤, 谷歌 aistudio 使用入口

最近的榜首又被Gemini给霸占了&#xff0c;很多童鞋想要体验一翻 Gemini免费库模型更新了 Gemini2.0向所有人开放了&#xff01;使用了真香 目前呢2.0flash和Gemini-2.0-Flash-Thinking-Exp、Gemini-2.0-Flash-Thinking-Exp-with-apps已经免费给所有注册用户开放了&#xff0c…

【数据结构】(7) 栈和队列

一、栈 Stack 1、什么是栈 栈是一种特殊的线性表&#xff0c;它只能在固定的一端&#xff08;栈顶&#xff09;进行出栈、压栈操作&#xff0c;具有后进先出的特点。 2、栈概念的例题 答案为 C&#xff0c;以C为例进行讲解&#xff1a; 第一个出栈的是3&#xff0c;那么 1、…

安宝特方案 | AR助力制造业安全巡检智能化革命!

引言&#xff1a; 在制造业中&#xff0c;传统巡检常面临流程繁琐、质量波动、数据难以追溯等问题。安宝特AR工作流程标准化解决方案&#xff0c;通过增强现实AR技术&#xff0c;重塑制造业安全巡检模式&#xff0c;以标准化作业流程为核心&#xff0c;全面提升效率、质量与…

语言月赛 202308【小粉兔做麻辣兔头】题解(AC)

》》》点我查看「视频」详解》》》 [语言月赛 202308] 小粉兔做麻辣兔头 题目描述 粉兔喜欢吃麻辣兔头&#xff0c;麻辣兔头的辣度分为若干级&#xff0c;用数字表示&#xff0c;数字越大&#xff0c;兔头越辣。为了庆祝粉兔专题赛 #1 的顺利举行&#xff0c;粉兔要做一些麻…

Dify Ollama本地私有化模型实践

今天给大家带来一篇deepseek本地部署&#xff0c;笔者最近由于研究AI大模型应用开发&#xff0c;笔记较少&#xff0c;后面将持续输出关于AI行业应用知识&#xff0c;请大家继续关注&#xff0c;话不多说&#xff0c;开始吧&#xff0c;啊哈哈。 DeepSeek 呢&#xff0c;最近十…

Kafka中的KRaft算法

我们之前的Kafka值依赖于Zookeeper注册中心来启动的&#xff0c;往里面注册我们节点信息 Kafka是什么时候不依赖Zookeeper节点了 在Kafka2.8.0开始就可以不依赖Zookeeper了 可以用KRaft模式代替Zookeeper管理Kafka集群 KRaft Controller和KRaft Leader的关系 两者关系 Lea…

GitPuk快速安装配置教程(入门级)

GitPuk是一款国产开源免费的代码管理工具&#xff0c;工具简洁易用&#xff0c;开源免费&#xff0c;本文将讲解如何快速安装和配置GitPuk&#xff0c;以快速入门上手。 1、安装 支持 Windows、Mac、Linux、docker 等操作系统。 1.1 Linux安装&#xfeff; 以下以Centos7安装…

2025年02月08日Github流行趋势

项目名称&#xff1a;anything-llm 项目地址url&#xff1a;https://github.com/Mintplex-Labs/anything-llm项目语言&#xff1a;JavaScript历史star数&#xff1a;34323今日star数&#xff1a;675项目维护者&#xff1a;timothycarambat, shatfield4, MrSimonC, franzbischof…

【C语言标准库函数】指数与对数函数:exp(), log(), log10()

目录 一、头文件 二、函数简介 2.1. exp(double x) 2.2. log(double x) 2.3. log10(double x) 三、函数实现&#xff08;概念性&#xff09; 3.1. exp(double x) 的模拟实现 3.2. log(double x) 和 log10(double x) 的模拟实现 四、注意事项 4.1. exp(double x) 的注…

Linux之kernel(1)系统基础理论(1)

Linux之Kernel(1)系统基础理论(1) Author: Once Day Date: 2025年2月6日 一位热衷于Linux学习和开发的菜鸟&#xff0c;试图谱写一场冒险之旅&#xff0c;也许终点只是一场白日梦… 漫漫长路&#xff0c;有人对你微笑过嘛… 全系列文章可参考专栏: Linux内核知识_Once-Day的…

从 Facebook 到元宇宙:社交网络的技术进化与前景

引言 社交网络的演变不仅仅是技术进步的体现&#xff0c;更是人类沟通方式革命的缩影。从 Facebook 的诞生到元宇宙的兴起&#xff0c;我们见证了社交互动从简单的信息交换到沉浸式虚拟体验的转变。本文将探讨这一技术演进的历程&#xff0c;并展望社交网络在元宇宙时代的新形…

内容中台赋能人工智能技术提升业务创新能力

内容概要 在当今快速变化的市场环境中&#xff0c;企业需要不断寻求创新以保持竞争力。内容中台作为一种新型的内容管理架构&#xff0c;能够极大地提升企业在内容创建、管理和分发方面的效率。通过与人工智能技术的深度融合&#xff0c;企业能够将海量的数据和信息转化为有价…

qt部分核心机制

作业 1> 手动将登录项目实现&#xff0c;不要使用拖拽编程 并且&#xff0c;当点击登录按钮时&#xff0c;后台会判断账号和密码是否相等&#xff0c;如果相等给出登录成功的提示&#xff0c;并且关闭当前界面&#xff0c;发射一个跳转信号&#xff0c;如果登录失败&#…