【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

相关推荐
祖国的好青年12 分钟前
VS Code 搭建 React Native 开发环境(Windows 实战指南)
android·windows·react native·react.js
黄林晴38 分钟前
警惕!AGP 9.2 别只改版本号,R8 规则与构建链路全线收紧
android·gradle
小米渣的逆袭1 小时前
Android ADB 完全使用指南
android·adb
儿歌八万首1 小时前
Jetpack Compose Canvas 进阶:结合 animateFloatAsState 让自定义图形动起来
android·动画·compose
zhangphil2 小时前
Android Page 3 Flow读sql数据库媒体文件,Kotlin
android·kotlin
神探小白牙2 小时前
echarts,3d堆叠图
android·3d·echarts
李白的天不白2 小时前
如何项目发布到github上
android·vue.js
summerkissyou19872 小时前
Android-RTC、NTP 和 System Time(系统时间)
android
小书房3 小时前
Kotlin使用体验及理解1
android·开发语言·kotlin
撩得Android一次心动3 小时前
Android Navigation 组件全面讲解
android·jetpack·navigation