【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

相关推荐
李斯维24 分钟前
循序渐进 Android Binder(二):传递自定义对象和 AIDL 回调
android·java·android studio
androidwork26 分钟前
OkHttp 3.0源码解析:从设计理念到核心实现
android·java·okhttp·kotlin
莉樱Yurin1 小时前
Kotlin/CLR 让Kotlin走进.NET世界
kotlin
像风一样自由1 小时前
【001】frida API分类 总览
android·frida
casual_clover1 小时前
Android 之 kotlin 语言学习笔记四(Android KTX)
android·学习·kotlin
移动开发者1号3 小时前
Android 大文件分块上传实战:突破表单数据限制的完整方案
android·java·kotlin
移动开发者1号3 小时前
单线程模型中消息机制解析
android·kotlin
每次的天空5 小时前
Android第十五次面试总结(第三方组件和adb命令)
android
追随远方5 小时前
Android音频开发:Speex固定帧与变长帧编解码深度解析
android·音视频
消失的旧时光-19436 小时前
Android和硬件通信
android