【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

相关推荐
c***21295 小时前
Springboot3学习(5、Druid使用及配置)
android·学习
修炼者5 小时前
【Android 进阶】别再强转 Context 了!手把手教你优雅解耦 View 与 Activity
android·android studio
x***01066 小时前
SpringSecurity+jwt实现权限认证功能
android·前端·后端
程序员江同学6 小时前
线下活动|2025 Kotlin 中文开发者大会北京分会场
android·kotlin
李坤林7 小时前
Android Vulkan 开启VK_GOOGLE_DISPLAY_TIMING 后,一个vsync 会释放两个imageBuffer现象分析
android·vulkan
Jerry7 小时前
Compose 状态思维
android
k***45998 小时前
MySQL----case的用法
android·数据库·mysql
r***86989 小时前
Plugin ‘mysql_native_password‘ is not loaded`
android·数据库·mysql
v***59839 小时前
MySQL-mysql zip安装包配置教程
android·mysql·adb
不用89k10 小时前
Android无法区分USB摄像头是哪一个
android