【kotlin协程】你以为的cancel也许并不cancel

【kotlin协程】你以为的cancel也许并不cancel

先贴上一句cancel名言:

当你调用 cancel() 后,Kotlin 协程的取消并不是立即生效的,特别是在某些情况下,例如当它正在进行的异步操作,而这些操作可能需要一些时间才能完成时。

当初次,甚至没遇到相关问题之前,看到这句话时都是云里雾里的,到底什么叫:协程的取消并不是立即生效的 ?

那么,接下来的几个例子,让你知道:为何我明明已经cancel了,被cancel协程块还在执行呢?

父子协程的关系

你爹永远是你爹,我们需要知道一个概念是,在某个 launch 内部进行 launch 时,当外部 launchcancel时,内部的 launch 也将被取消,代码层面来理解就是:

kotlin 复制代码
 fun main() {
     val scope = CoroutineScope(Dispatchers.Default)
     scope.launch {
         val jobParent = launch {
             launch {
                 repeat(10) {
                     println("child running, isActive: $isActive")
                     delay(1000L)
                 }
                 println("child end")
             }
 ​
             while (true) {
                 println("parent running, isActive: $isActive")
                 delay(1000L)
             }
         }
 ​
         // helper launch
         launch {
             delay(5000L) // 等待父子协程执行5秒后, 取消父协程
             jobParent.cancel()
             println("end, jobParent isActive: ${jobParent.isActive}")
         }
     }
 ​
     // Thread.sleep(5000L) //等待5秒, 避免协程未开启程序就已经结束
     while (true);
 }

输出结果如下(注意,因为协程的调度原因,输出顺序并不完全如下):

text 复制代码
 parent running, isActive: true
 child running, isActive: true
 parent running, isActive: true
 child running, isActive: true
 parent running, isActive: true
 child running, isActive: true
 parent running, isActive: true
 child running, isActive: true
 parent running, isActive: true
 child running, isActive: true
 end, jobParent isActive: false

当5秒后,父协程被cancel了,此时内部的子协程也随着 jobParent 的取消而取消了。

让人误解的 cancel

以上代码逻辑看起来似乎也没什么毛病,该取消的和不该取消的都被取消了。

那么,接下来,进入今天的主题,cancel 为何并不 cancel

将上面的代码稍微修改一下,来达到调用 cancel 之后,协程内部逻辑仍在继续执行的情况。

kotlin 复制代码
 fun main() {
     val scope = CoroutineScope(Dispatchers.Default)
     scope.launch {
         val jobParent = launch {
             launch {
                 repeat(10) {
                     println("child running, isActive: $isActive")
                     // 这里调用线程的睡眠, 来模拟第三方库的耗时请求 (如:间隔1秒钟输出一行文本内容的业务逻辑)
                     runCatching { Thread.sleep(1000) }
                 }
                 println("child end")
             }
 ​
             while (true) {
                 println("parent running, isActive: $isActive")
                 delay(1000L)
             }
         }
 ​
         // helper launch
         launch {
             delay(3000L) // 等待父子协程执行3秒后, 取消父协程
             jobParent.cancel()
             println("end, jobParent isActive: ${jobParent.isActive}")
         }
     }
 ​
     // Thread.sleep(5000L) //等待5秒, 避免协程未开启程序就已经结束
     while (true);
 }

为了避免过渡的等待,这里将 helper launch 的等待改为了3秒,并修改内部子协程的逻辑,模拟第三方库的耗时请求。

按照惯性思维,当 jobParent.cancel() 被调用后,所有的协程都应该被取消,输出的内容应该和前文并无二致才对。

然而事实却并非如此。

以下是修改后输出的内容(注意,因为协程的调度原因,输出顺序并不完全如下):

text 复制代码
 parent running, isActive: true
 child running, isActive: true
 child running, isActive: true
 parent running, isActive: true
 child running, isActive: true
 parent running, isActive: true
 end, jobParent isActive: false
 child running, isActive: false
 child running, isActive: false
 child running, isActive: false
 child running, isActive: false
 child running, isActive: false
 child running, isActive: false
 child running, isActive: false
 child end

没错,协程的确是结束了。但,耗时操作却仍在执行,尽管父协程已经被取消了, isActive 已经不再保持活跃。

但,它还在执行。

原来, 协程的取消并不是立即生效的 这句话是这么理解的呀。

真是小刀喇屁股,开了眼了。

isActive的妙用

正如上面的输出内容所示,我们可以发现当协程结束之后,不论父子,它们的协程活跃状态 isActive 都变为了 false (事实上,父协程已经被正常cancel掉了) 。

那么?

是否就可以在这isActive上下文章了?

肯定的,我们需要让三方库的耗时操作,在协程不再活跃时,也就是 isActive == false 时终止它。

于是,就可以这么书写。

kotlin 复制代码
 fun main() {
     val scope = CoroutineScope(Dispatchers.Default)
     scope.launch {
         val jobParent = launch {
             launch {
                 repeat(10) {
                     if (!isActive) //当协程不再活跃状态时,手动结束
                         return@repeat
 ​
                     println("child running, isActive: $isActive")
                     // 这里调用线程的睡眠, 来模拟第三方库的耗时请求 (如:间隔1秒钟输出一行文本内容的业务逻辑)
                     runCatching { Thread.sleep(1000) }
                 }
                 println("child end")
             }
 ​
             while (true) {
                 println("parent running, isActive: $isActive")
                 delay(1000L)
             }
         }
 ​
         // helper launch
         launch {
             delay(3000L) // 等待父子协程执行3秒后, 取消父协程
             jobParent.cancel()
             println("end, jobParent isActive: ${jobParent.isActive}")
         }
     }
 ​
     // Thread.sleep(5000L) //等待5秒, 避免协程未开启程序就已经结束
     while (true);
 }

输出的内容如下(注意,因为协程的调度原因,输出顺序并不完全如下):

text 复制代码
 child running, isActive: true
 parent running, isActive: true
 child running, isActive: true
 parent running, isActive: true
 child running, isActive: true
 parent running, isActive: true
 end, jobParent isActive: false
 child end

这时,cancel 才算得上是真正意义上的 cancel

相关推荐
devlei3 小时前
从源码泄露看AI Agent未来:深度对比Claude Code原生实现与OpenClaw开源方案
android·前端·后端
阿拉斯攀登6 小时前
从入门到实战:CMake 与 Android JNI/NDK 开发全解析
android·linux·c++·yolo·cmake
冬奇Lab7 小时前
相机录像流程:MediaRecorder与Camera2的协作之道
android·音视频开发·源码阅读
麦客奥德彪7 小时前
Jetpack Compose 常用开发总结
android
麦客奥德彪8 小时前
Jetpack Compose Modifier 完全指南
android
Mac的实验室10 小时前
(2026年最新)解决谷歌账号注册设备扫码短信发送失败无法验证难题(100%通过无需扫码验证)
android·google·程序员
半条咸鱼11 小时前
如何通过 ADB 连接安卓设备(USB + 无线 TCP/IP)
android
huwuhang11 小时前
斐讯盒子N1_YYFROM固件_webview119更新版附安卓专用遥控器刷机固工具USB_Burning_Tool
android
qq_3520186811 小时前
android 状态栏高度获取
android
AirDroid_cn11 小时前
安卓15平板分屏比例能到1:9吗?极限分屏设置教程
android·智能手机