Fork me on GitHub

协程中的取消和异常 (取消操作详解)

正文

在开发中,我们要避免不必要的的任务来节约设备的内存和电量的使用,协程也是如此。在使用的过程我们需要控制好它的生命周期,在不需要它的取消它。

调用cancel方法

取消作用域会取消它的子协程

当启动了很多个协程,我们一个个协程的取消比较麻烦,我们可以通过取消整个作用域来解决这个问题,因为取消作用域可以取消该作用域创建的所有协程。

1
2
3
4
5
6
/ 假设我们已经定义了一个作用域

val job1 = scope.launch { … }
val job2 = scope.launch { … }

scope.cancel()

假设我们创建了一个作用域scope,并创建了两个协程job1和job2。我们通过调用scope.cancel(),取消作用域,将会把job1 和job2两个协程都取消。

单独取消某个协程,不会影响他的兄弟协程

我们创建了两个协程,job1和job2.我们单独取消job1,不会影响到job2

1
2
3
4
5
6
7
// 假设我们已经定义了一个作用域

val job1 = scope.launch { … }
val job2 = scope.launch { … }

// 第一个协程将会被取消,而另一个则不受任何影响
job1.cancel()

协程通过抛出一个特殊的异常 CancellationException 来处理取消操作

在调用cancel函数的时候,我们需要传入一个CancellationException对象,如果我们没有传入,那就用默认的defaultCancellationException。

1
2
3
4
// external cancel with cause, never invoked implicitly from internal machinery
public override fun cancel(cause: CancellationException?) {
cancelInternal(cause ?: defaultCancellationException())
}

一旦抛出了CancellationException,我们就可以通过这一机制来处理协程的取消。在底层的实现中,子协程会通过抛出异常的方式将取消的情况通知它的父级,父协程通过传入的取消原因决定是否处理该异常。

不能在已取消的作用域中再次启动新的协程

调用了 cancel 方法为什么协程处理的任务没有停止?

不同的Diapatcher不同的区别,下一篇文章将介绍。
我们以Dispatchers.Default为例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import kotlinx.coroutines.*

suspend fun main() = runBlocking {
var startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextTime = startTime
var i = 0
while (i < 5) {
if (System.currentTimeMillis() >= nextTime) {
println("这是第${i}次")
i++
//1000毫秒执行一次
nextTime += 1000
}
}
}
delay(1000)
println("取消")
job.cancel()
println("取消完毕")

}
1
2
3
4
5
6
7
这是第0
这是第1
取消
取消完毕
这是第2
这是第3
这是第4

调用cancel方法之后,协程的任务依然在运行。调用cancel方法的时候,此时协程处于cancelling正在取消的状态,接着我们打印了2,3,4,处理任务结束之后,协程变成cancelled已经取消的状态,这是以Default举例,Default调度会等待协程任务处理完毕才取消。

让协程可以被取消

协程处理任务都是协作式的,协作的意思就是我们的处理任务要配合协程取消做处理。因此在执行任务期间我们要定时检查协程的状态是否已经取消,例如我们从磁盘读取文件之前我们先检查协程是否被取消了。

1
2
3
4
5
6
val job = launch {
for(file in files) {
// TODO 检查协程是否被取消
readFile(file)
}
}

协程中的挂起函数都是可取消的,使用他们的时候,我们不需要检查协程是否已取消。例如withContext,delay 。如果没有这些挂起函数,为了让我们的代码配合协程取消,可以使用一下两种方法:

  • 检查 job.isActive 或者使用 ensureActive()
  • 使用 yield() 来让其他任务进行

检查 job 的活跃状态

先看一下第一种方法,在我们的 while(i<5) 循环中添加对于协程状态的检查:

1
2
// 因为处于 launch 的代码块中,可以访问到 job.isActive 属性
while (i < 5 && isActive)

使用 yield() 函数运行其他任务

Job.join 和 Deferred.await cancellation

等待协程处理结果有两种方法,launch启动的job可以调用join,async 返回的Deferred 可以调用await方法

  • job.join会让协程挂起,直到等待协程处理任务完毕,我们可以配合cancel使用
  • deferred.await()如果我们关心协程的处理结果,我们可以使用deferred。结果由deferred.await返回。也是job类型,也可以被取消。

处理协程取消的副作用

当我们需要在协程取消 后处理一些清理的工作,或者做一些打印日志。我们有几种办法:

  • 通过检查协程的状态
1
2
3
4
5
6
7
8
9
while (i < 5 && isActive) {
if (…) {
println(“Hello ${i++}”)
nextPrintTime += 500L
}
}

// 协程所处理的任务已经完成,因此我们可以做一些清理工作
println(“Clean up!”

当判断协程不是isActive状态的时候,我们可以做一些清理

  • try catch finally
    我们知道协程的取消会抛出CancellationException 异常,我们可以在协程提中使用try catch finally,在finally中做我们的一些清理的工作,或者打印日志
1
2
3
4
5
6
7
8
9
10
11
12
13
14
val job = launch {
try {
work()
} catch (e: CancellationException){
println(“Work cancelled!”)
} finally {
println(“Clean up!”)
}
}

delay(1000L)
println(“Cancel!”)
job.cancel()
println(“Done!

已经取消的协程,不能再被挂起

已经取消的协程,不能再被挂起,但是当我们需要在取消的协程中调用挂起函数,那么我们可以在finally中使用NonCancellable ,意思是让协程挂起,直到处理挂起函数中的代码完毕,协程才会取消。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
val job = launch {
try {
work()
} catch (e: CancellationException){
println(“Work cancelled!”)
} finally {
withContext(NonCancellable){
delay(1000L) // 或一些其他的挂起函数
println(“Cleanup done!”)
}
}
}

delay(1000L)
println(“Cancel!”)
job.cancel()
println(“Done!

在jetpack中使用viewModelScope 或者lifecycleScope 中定义的作用域,他们在scope完成后取消他们的处理任务。如果我们手动创建自己的作用域CoroutineScope,我们需要协作协程,将我们的作用域和job绑定,在需要的时候取消。

,