原文作者 :Florina Muntenescu
原文地址: Cancellation and Exceptions in Coroutines (Part 2)
译者 : 京平城
Calling cancel
当launching多个coroutines的时候,一个个的取消它们将会十分麻烦。我们可以通过调用scope的cancel()方法来取消所有的子coroutines:
// assume we have a scope defined for this layer of the app
val job1 = scope.launch { … }
val job2 = scope.launch { … }
scope.cancel()
Cancelling the scope cancels its children
但有的时候,你可能希望取消其中的一个coroutine而不要影响到其它的子coroutines:
// assume we have a scope defined for this layer of the app
val job1 = scope.launch { … }
val job2 = scope.launch { … }
// First coroutine will be cancelled and the other one won’t be affected
job1.cancel()
A cancelled child doesn’t affect other siblings
Coroutines的取消机制是通过抛出一个特殊的异常:CancellationException。
你可以在cancel()方法中传入一个CancellationException实例来记录详细的异常信息:
fun cancel(cause: CancellationException? = null)
如果你不传入一个CancellationException实例的话,那么cancel()方法默认也会创建一个CancellationException实例(详细代码):
public override fun cancel(cause: CancellationException?) {
cancelInternal(cause ?: defaultCancellationException())
}
你可以通过捕获CancellationException来处理coroutine的取消。
child job的取消将通过抛出这个exception来通知它的parent job。Parent job通过判断取消的原因来决定是否需要处理这个异常,如果child job的抛出的是CancellationException,那么parent job不需要做出额外的处理。
⚠️Once you cancel a scope, you won’t be able to launch new coroutines in the cancelled scope.
在Android开发中,如果你已经使用了KTX扩展包,那么你不需要创建自定义的CoroutineScope,也不需要手动去处理coroutines的取消。
举例来说,当你使用ViewModel的时候,我们提供了ViewModel的扩展属性viewModelScope。
当你想创建一个具有生命周期感知能力的scope的时候,可以使用lifecycleScope。
viewModelScope和lifecycleScope都会在正确的时机自动cancel还在执行中的coroutines。具体可以参考[译]Coroutines in Android ViewModelScope篇
译者注:有一种特殊情况,你可能不希望coroutines被自动cancel,比如要发送日志给后端的服务器,又或者一些重要的操作一定需要执行完成,那么可以使用GlobalScope.launch {},但是要注意它的生命周期和你的应用一样长,使用的时候需要非常谨慎,防止内存泄露。
另外本文后面也介绍了另外一种在coroutines被cancelled的情况下,继续执行可挂起代码的方法。
Why isn’t my coroutine work stopping?
单单只是调用了cancel方法,并不意味着你的coroutines就突然停止执行剩余代码了。比如当你批量读取文件的时候,调用cancel方法,你的coroutine是不会立即停止工作的。
译者注:这个和Java中Thread的中断机制是一样的,调用Thread的interrupt()方法的同时,还需要配合检查isInterrupted()来优雅的处理线程的退出。
让我们来看一个例子:
import kotlinx.coroutines.*
fun main(args: Array<String>) = runBlocking<Unit> {
val startTime = System.currentTimeMillis()
val job = launch (Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) {
// print a message twice a second
if (System.currentTimeMillis() >= nextPrintTime) {
println("Hello ${i++}")
nextPrintTime += 500L
}
}
}
delay(1000L)
println("Cancel!")
job.cancel()
println("Done!")
}
输入结果是:
Hello 0
Hello 1
Hello 2
Cancel!
Done!
Hello 3
Hello 4
都已经执行了job.cancel(),为什么Hello 3和Hello 4还是打印了呢?
其实当job.cancel()调用的时候,虽然coroutine的状态变成Cancelling了,但是剩余代码还是会继续执行,直到所有的工作完成。
Cancellation of coroutine code needs to be cooperative!
Cancellation of coroutine code needs to be cooperative!
coroutine的退出必须是协作式的,你需要定期的去检查coroutine的状态:
val job = launch {
for(file in files) {
// TODO 在这里检查是否Job已经被cancel, 是否需要执行剩余代码
readFile(file)
}
}
kotlinx.coroutines包下的所有suspend方法都是可取消的:比如withContext, delay等。
当你使用这些可取消方法的时候,你不需要检查coroutines是否已经被cancel了又或者抛出CancellationException了。但是,如果你没有使用这些方法,你需要自己去处理coroutines的取消:
- 检查状态
job.isActive或者ensureActive() - 或者使用
yield()方法 译者注:yield方法中会调用context.checkCompletion(),ensureActive()和yield()都会检查Job的state是否还是active的,如果不是会抛出CancellationException来终止coroutines:
public fun Job.ensureActive(): Unit {
if (!isActive) throw getCancellationException()
}
internal fun CoroutineContext.checkCompletion() {
val job = get(Job)
if (job != null && !job.isActive) throw job.getCancellationException()
}
Checking for job’s active state
回到刚才的例子,我们可以通过在while循环中访问job的isActive属性(译者注:isActive是CoroutineScope的一个扩展属性,该扩展属性直接返回了Job的isActive属性)来检查coroutine是否已经被取消:
// Since we're in the launch block, we have access to job.isActive
while (i < 5 && isActive)
这样while循环内的代码只有当job仍然active的时候才会执行,并且我们可以在while循环结束后通过!isActive条件判断来执行一些自定义的任务,比如记录Job被cancelled的日志。
还有一种做法是在while循环内使用帮助方法ensureActive()
while (i < 5) {
ensureActive()
…
}
这样做的好处是不需要直接访问isActive属性(译者注:封装性更好,方法内封装了具体实现,如果将来修改了实现也不需要修改代码),ensureActive()方法会在Job被cancelled的时候立刻抛出CancellationException,来保证剩余的代码不会被执行。
但是使用ensureActive()的话,在某些情况下会失去代码控制上的灵活性,比如在which循环结束的时候通过!isActive条件判断来记录Job的取消日志。
Let other work happen using yield()
什么情况下我们应该使用yield()方法呢?
- 繁重的CPU计算任务。
- 有可能会把thread pool的资源耗尽的任务。
- 希望thread pool中的线程能更好的协作而不只是通过添加更多的线程到thread pool中的时候。
在这种情况下你可以使用
yield()方法来周期性的检查Job的状态,就像刚才例子中的ensureActive()方法一样。
Job.join vs Deferred.await cancellation
获取coroutine返回值的方法有两种:
job.join()方法async{}.wait()方法,async{}返回的是一个Deferred类型的Job
job.join()方法会暂停coroutine直到工作完成,当它和job.cancel一起使用的时候会产生如下效果:
- 先调用
job.cancel后调用job.join(),那么挂起的job不会被执行完成。 译者注: 这里原文似乎有问题,原文是If you’re calling job.cancel then job.join, the coroutine will suspend until the job is completed.
但是job.join()执行的时候会判断Job的状态是否是active的,如果不是就直接return了,所以 如果job.join()放在job.cancel后面的话,那么Job就是直接不执行后面代码了。
// join的相关实现在JobSupport.kt中
public final override suspend fun join() {
if (!joinInternal()) { // fast-path no wait
coroutineContext.checkCompletion()
return // do not suspend
}
return joinSuspend() // slow-path wait
}
- 先调用
job.join()后调用job.cancel的话,那么cancel无效,因为coroutine已经执行完毕了。
下面分析一下通过async方法创建的Deferred类型的Job和cancel()方法一起使用的效果:
val deferred = async { … }
deferred.cancel()
val result = deferred.await() // throws JobCancellationException!
- 先调用
cancel()方法后调用await()方法会直接抛出异常:JobCancellationException: Job was cancelled - 先调用
await()方法后调用cancel()方法,什么事都不会发生,因为coroutine已经执行完毕了。
Handling cancellation side effects
如果你想在coroutine取消后执行某些操作:
比如关闭某些正在使用的资源,记录取消日志又或者执行一些cleanup的代码等等。
Check for !isActive
你可以周期性的检查isActive属性,一旦跳出了while循环,就执行clean up操作:
while (i < 5 && isActive) {
// print a message twice a second
if (…) {
println(“Hello ${i++}”)
nextPrintTime += 500L
}
}
// the coroutine work is completed so we can cleanup
println(“Clean up!”)
Try catch finally
因为coroutine被取消的时候会抛出CancellationException,那么我们可以捕获这个异常,并且在finally块中执行clean up操作:
val job = launch {
try {
work()
} catch (e: CancellationException){
println(“Work cancelled!”)
} finally {
println(“Clean up!”)
}
}
delay(1000L)
println(“Cancel!”)
job.cancel()
println(“Done!”)
要注意的是一旦coroutine处于取消的状态,那么接下去要执行的代码就不会被挂起了(suspend)。
A coroutine in the cancelling state is not able to suspend!
如果在clean up操作中,你想继续执行可以被挂起的代码,那么你可以使用NonCancellable这个CoroutineContext。
它将维持coroutine的取消状态直到被挂起的的代码执行完毕:
val job = launch {
try {
work()
} catch (e: CancellationException){
println(“Work cancelled!”)
} finally {
withContext(NonCancellable){
delay(1000L) // or some other suspend fun
println(“Cleanup done!”)
}
}
}
delay(1000L)
println(“Cancel!”)
job.cancel()
println(“Done!”)
suspendCancellableCoroutine and invokeOnCancellation
使用suspendCancellableCoroutine和continuation.invokeOnCancellation来处理coroutines的回调(译者注:类似于给coroutines的生命周期发生变化的时候添加回调函数)
suspend fun work() {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
// do cleanup
}
// rest of the implementation
}
译者注:suspendCancellableCoroutine是可以通过job.cancel()来取消的。
总结:
取消coroutines是门学问,请尽量使用viewModelScope或者lifecycleScope。