【kotlin协程】你以为的cancel也许并不cancel
先贴上一句cancel
名言:
当你调用
cancel()
后,Kotlin 协程的取消并不是立即生效的,特别是在某些情况下,例如当它正在进行的异步操作,而这些操作可能需要一些时间才能完成时。
当初次,甚至没遇到相关问题之前,看到这句话时都是云里雾里的,到底什么叫:协程的取消并不是立即生效的
?
那么,接下来的几个例子,让你知道:为何我明明已经cancel
了,被cancel
协程块还在执行呢?
父子协程的关系
你爹永远是你爹,我们需要知道一个概念是,在某个 launch
内部进行 launch
时,当外部 launch
被cancel
时,内部的 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
。