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 {
            //sampleStart
            val 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 bit
            Log.d(TAG,"main: I'm tired of waiting!")
            job.cancelAndJoin() // cancels the job and waits for its completion
            Log.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 {
            //sampleStart
            val startTime = System.currentTimeMillis()
            val job = launch(Dispatchers.Default) {
                var nextPrintTime = startTime
                var i = 0
                while (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 {
            //sampleStart
            val 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 {//sampleStart
            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)
                        //如果不用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 {//sampleStart
            try {
                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 {//sampleStart
            try {
                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 {//sampleStart
            repeat(10000){
                launch {
                    var res = withTimeout(60){
                        delay(50)
                        Resource()
                    }
                    res.close()
                }
            }
        }
        Log.d(TAG,"aq:$aq")




 class Resource{
        var aq = 0
        init{
            aq++
        }
        fun close(){
            aq--
        }
    }

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

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

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

runBlocking {//sampleStart
            repeat(10000){
                launch {
                    var res: Resource? = null
                    try {
                        res = withTimeout(60){
                            delay(50)
                            Resource()
                        }
                    }finally {
                        res?.close()
                    }
                }
            }
        }
        Log.d(TAG,"aq:$aq")

class Resource{
    var aq = 0
    init{
        aq++
    }
    fun close(){
        aq--
    }
}

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

推荐文章

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++小等于的所有奇数和=最大奇数除2加1的平方。

缘由 三种思路解题&#xff1a;依据算术推导得到一个规律&#xff1a;小等于的所有奇数和等于最大奇数除以2加1的平方。将在后续发布&#xff0c;总计有十种推导出来的实现代码。 int a 0,aa 1,aaa 0;cin >> a; while (aa<a) aaa aa, aa 2;cout << aaa;i…

【CPP】CPP经典面试题

文章目录 引言1. C 基础1.1 C 中的 const 关键字1.2 C 中的 static 关键字 2. 内存管理2.1 C 中的 new 和 delete2.2 内存泄漏 3. 面向对象编程3.1 继承和多态3.2 多重继承 4. 模板和泛型编程4.1 函数模板4.2 类模板 5. STL 和标准库5.1 容器5.2 迭代器 6. 高级特性6.1 移动语义…

深入浅出谈VR(虚拟现实、VR镜头)

1、VR是什么鬼&#xff1f; 近两年VR这次词火遍网上网下&#xff0c;到底什么是VR&#xff1f;VR是“Virtual Reality”&#xff0c;中文名字是虚拟现实&#xff0c;是指采用计算机技术为核心的现代高科技手段生成一种虚拟环境&#xff0c;用户借助特殊的输入/输出设备&#x…

【Redis】安装配置Redis超详细教程 / Linux版

Linux安装配置Redis超详细教程 安装redis依赖安装redis启动redis停止redisredis.conf常见配置设置redis为后台启动修改redis监听地址设置工作目录修改密码监听的端口号数据库数量设置redis最大内存设置日志文件设置redis开机自动启动 学习视频&#xff1a;黑马程序员Redis入门到…

[LeetCode]day16 242.有效的字母异位词

242. 有效的字母异位词 - 力扣&#xff08;LeetCode&#xff09; 题目描述 给定两个字符串 s 和 t &#xff0c;编写一个函数来判断 t 是否是 s 的 字母异位词 示例 1: 输入: s "anagram", t "nagaram" 输出: true示例 2: 输入: s "rat"…

[MoeCTF 2022]baby_file

题目 <html> <title>Heres a secret. Can you find it?</title> <?phpif(isset($_GET[file])){$file $_GET[file];include($file); }else{highlight_file(__FILE__); } ?> </html> 读取flag /?filephp://filter/readconvert.base64-encode…

Centos挂载镜像制作本地yum源,并补装图形界面

内网环境centos7.9安装图形页面内网环境制作本地yum源 上传镜像到服务器目录 创建目录并挂载镜像 #创建目录 cd /mnt/ mkdir iso#挂载 mount -o loop ./CentOS-7-x86_64-DVD-2009.iso ./iso #前面镜像所在目录&#xff0c;后面所挂载得目录#检查 [rootlocalhost mnt]# df -h…

判断您的Mac当前使用的是Zsh还是Bash:echo $SHELL、echo $0

要判断您的Mac当前使用的是Zsh还是Bash&#xff0c;可以使用以下方法&#xff1a; 查看默认Shell: 打开“终端”应用程序&#xff0c;然后输入以下命令&#xff1a; echo $SHELL这将显示当前默认使用的Shell。例如&#xff0c;如果输出是/bin/zsh&#xff0c;则说明您使用的是Z…

python 小游戏:扫雷

目录 1. 前言 2. 准备工作 3. 生成雷区 4. 鼠标点击扫雷 5. 胜利 or 失败 6. 游戏效果展示 7. 完整代码 1. 前言 本文使用 Pygame 实现的简化版扫雷游戏。 如上图所示&#xff0c;游戏包括基本的扫雷功能&#xff1a;生成雷区、左键点击扫雷、右键标记地雷、显示数字提示…

【重新认识C语言----文件管理篇】

目录 ​编辑 -----------------------------------------begin------------------------------------- 引言 1. 文件的基本概念 2. 文件指针 3. 文件的打开与关闭 3.1 打开文件 3.2 关闭文件 4. 文件的读写操作 4.1 读取文件 4.1.1 使用fgetc()读取文件 4.1.2 使用fg…

EasyExcel 导出合并层级单元格

EasyExcel 导出合并层级单元格 一、案例 案例一 1.相同订单号单元格进行合并 合并结果 案例二 1.相同订单号的单元格进行合并2.相同订单号的总数和总金额进行合并 合并结果 案例三 1.相同订单号的单元格进行合并2.相同订单号的商品分类进行合并3.相同订单号的总数和总金额…

WPF 进度条(ProgressBar)示例一

本文讲述&#xff1a;WPF 进度条(ProgressBar)简单的样式修改和使用。 进度显示界面&#xff1a;使用UserControl把ProgressBar和进度值以及要显示的内容全部组装在UserControl界面中&#xff0c;方便其他界面直接进行使用。 <UserControl x:Class"DefProcessBarDemo…

LabVIEW自定义测量参数怎么设置?

以下通过一个温度采集案例&#xff0c;说明在 LabVIEW 中设置自定义测量参数的具体方法&#xff1a; 案例背景 ​ 假设使用 NI USB-6009 数据采集卡 和 热电偶传感器 监测温度&#xff0c;需自定义以下参数&#xff1a; 采样率&#xff1a;1 kHz 输入量程&#xff1a;0~10 V&a…

新能源产业的质量革命:六西格玛培训如何重塑制造竞争力

在新能源行业狂飙突进的今天&#xff0c;企业若想在全球供应链中占据高地&#xff0c;仅靠技术突破已远远不够。制造效率的毫厘之差&#xff0c;可能成为市场话语权的千里之距。某光伏巨头曾因电池片良率低于行业均值1.5%&#xff0c;导致年损失超2.3亿元——这恰恰印证了六西格…

(11)gdb 笔记(4):设置执行方向 set exec-direction,

&#xff08;28&#xff09;引入 record 后&#xff0c;可以 设置执行方向 set exec-direction &#xff1a; 实践&#xff1a; &#xff08;29&#xff09; &#xff08;33&#xff09; 谢谢

redis持久化理论

0 前言 什么是持久化 redis操作都是在内存中&#xff0c;如果出现宕机的话&#xff0c;数据将不复存在&#xff0c;所以持久化是将内存中的数据刷盘到磁盘中&#xff0c;redis可以提供RDB和AOF将数据写入磁盘中。 一 持久化技术 本章节将介绍持久化RDB和AOF两个技术&#xf…

25/2/7 <机器人基础>雅可比矩阵计算 雅可比伪逆

雅可比矩阵计算 雅可比矩阵的定义 假设我们有一个简单的两个关节的平面机器人臂&#xff0c;其末端执行器的位置可以表示为&#xff1a; 其中&#xff1a; L1​ 和 L2 是机器人臂的长度。θ1​ 和 θ2是关节的角度。 计算雅可比矩阵 雅可比矩阵 JJ 的定义是将关节速度与末…

鸿蒙UI(ArkUI-方舟UI框架)- 使用文本

返回主章节 → 鸿蒙UI&#xff08;ArkUI-方舟UI框架&#xff09; 文本使用 文本显示 (Text/Span) Text是文本组件&#xff0c;通常用于展示用户视图&#xff0c;如显示文章的文字内容。Span则用于呈现显示行内文本。 创建文本 string字符串 Text("我是一段文本"…

科技赋能数字内容体验的核心技术探索

内容概要 在数字化时代&#xff0c;科技的迅猛发展为我们的生活和工作带来了深刻的变革。数字内容体验已经成为人们获取信息和娱乐的重要途径&#xff0c;而这背后的技术支持则扮演着至关重要的角色。尤其是在人工智能、虚拟现实和区块链等新兴技术的推动下&#xff0c;数字内…

详细教程 | 如何使用DolphinScheduler调度Flink实时任务

Apache DolphinScheduler 非常适用于实时数据处理场景&#xff0c;尤其是与 Apache Flink 的集成。DolphinScheduler 提供了丰富的功能&#xff0c;包括任务依赖管理、动态调度、实时监控和日志管理&#xff0c;能够有效简化 Flink 实时任务的管理和部署。通过 DolphinSchedule…