前言
翻译自Part 2: Cancellation in coroutines
取消协程
1当运行多个协程时,跟踪他们或者单独取消是一个痛点。我们可以依赖启动协程的scope来取消创建的所有协程。例如
val job1 = scope.launch { … }
val job2 = scope.launch { … }
scope.cancel()
取消了 scope 也取消了它的所有子级。
有时可能只需要取消一个协程,也许是对用户输入做出反应。例如下面只取消job1,这并不影响其他的协程:
val job1 = scope.launch { … }
val job2 = scope.launch { … }
job1.cancel()
协程通过引发特殊异常CancellationException来处理取消。如果想要提供更多的取消理由的详细信息,可以在调用cancel方法的时候提供一个CancellationException的实例
fun cancel(cause: CancellationException? = null)
如果你不提供自己的CancellationException实例,会创建一个默认的CancellationException
public override fun cancel(cause: CancellationException?) {
cancelInternal(cause ?: defaultCancellationException())
}
因为会抛出CancellationException,需要去处理取消,至于如何处理后续会讲。
在后台,子job通过这个异常来通知它的父级有关取消的操作,父级通过cancel的原因来确定是否需要它来处理异常。如果子级由于CancellationException取消了,父级不要求做其他的操作。
一旦你取消了一个scope,就不能再使用这个scope创建新协程。
如果你使用了androidx KTX 库,那你不需要创建自己的scope,因为也不需要去取消他们。viewmodelScope和lifecyclescope都会在正确的时机取消。
为什么我的协程不停止工作
如果我们只调用了cancel,这不意味着协程工作将停止。如果你在进行一些比较繁重的计算,例如从多个文件中读取,没有什么可以阻止你的代码运行。因此协程的取消代码需要合作,需要去判断协程是否在定期活动。
你需要确保所有执行的协程工作和取消合作,因此你需要不断地或者在进行任何长时间工作前判断是否取消了协程。例如:
val job = launch {
for(file in files) {
//TODO 判断是否取消
readFile(file)
}
}
所有来自kotlinx.coroutines的suspend函数是可取消的:withContext、delay等。所以如果你使用任意一个,则不需要去判断取消且停止运行或抛出CancellationException。但是。如果你没有用他们,请保证你的协程代码由于这两个选项合作了:
- 判断 job.iaActive 或 ensureActive()
- 让其他工作发生:yield()
判断job的活动状态
- iaActive
while (i < 5 && isActive)
2.ensureActive()
这个方法的实现方式是这样的
fun Job.ensureActive(): Unit {
if (!isActive) {
throw getCancellationException()
}
}
使用方法
while (i < 5) {
ensureActive()
…
}
使用yield()让其他工作发生
如果你在做的工作是这样的: 1. 重CPU 2. 可能耗尽线程池 3.想要不添加新的线程到线程池并允许这个线程去做其他的工作 ,那么就用yield()。如果job已经完成,yield()将检查完成情况并抛出CancellationException退出协程。yield应该是定期检查中的第一个操作,类似上面提到的ensureActive()
val job = launch (Dispatchers.Default) {
val startTime = System.currentTimeMillis()
var nextPrintTime = startTime
var i = 0
while (i < 5) {
yield() // yield
// 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!
如果不使用yield输出
Hello 0
Hello 1
Hello 2
Cancel!
Done!
Hello 3
Hello 4
job.join VS Deferred.await 取消
这里有两种方法等待协程的结果:从launch返回的jobs可以调用join 、async返回的Deferred(job的一种类型)可以调用await
job.join暂停协程直到工作完成。和job.cancel一起使用,它的行为符合预期:
- 如果先调用job.cancel再调用job.join ,协程会暂停直至job完成。
- 先调用job.join再调用job.cancel是无效的,因为这个job已经完成了。
note: 这里要注意如果协程中使用了withContext、delay等这些,job.cancel就会cancel掉job的工作的。
val job = launch(Dispatchers.IO) {
val startTime = System.currentTimeMillis()
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(1000)
println("cancel前" + job2.isCompleted + job2.isCancelled)
job.cancel()
println("cancel后join前" + job2.isCompleted + job.isCancelled)
job.join()
println("join后" + job2.isCompleted + job2.isCancelled)
输出
Hello 0
Hello 1
cancel前falsefalse
Hello 2
cancel后join前falsetrue
Hello 3
Hello 4
join后truetrue
如果需要协程的返回值用Deferred。当协程完成的时候,deferred.await将结果返回。Deferred是一种job类型,也可以被取消。
val deferred = async { … }
deferred.cancel()
val result = deferred.await() // throws JobCancellationException!
因为await的作用是去暂停协程直至结果被计算完成,一旦协程被取消就无法获得结果,因此,会抛出JobCancellationException异常。
处理取消的副作用
如果当协程取消后想进行一些具体的操作,例如关闭你再用的任何资源,打印取消日志或者其他你想运行的代码。这里有一些方法可以做到这一点:
判断iaActive
如果你周期性判断isaCTIVE,一旦你跳出循环,就清空资源。例如
val startTime = System.currentTimeMillis()
val job = launch (Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5 && isActive) {
if (System.currentTimeMillis() >= nextPrintTime) {
println("Hello ${i++}")
nextPrintTime += 500L
}
}
println("Clean up!")
}
delay(1000L)
println("Cancel!")
job.cancel()
println("Done!")
try catch finally
val job = launch (Dispatchers.Default) {
try {
work()
} catch (e: CancellationException){
println("Work cancelled!")
} finally {
println("Clean up!")
}
}
delay(1000L)
println("Cancel!")
job.cancel()
println("Done!")
但是取消状态下的协程不能调用suspend函数,为了在协程取消后能够切换到清理任务上,我们需要使用NonCancellable CoroutineContext,这允许我们在清理工作完成前让协程保持在cancelling状态。
val job = launch (Dispatchers.Default) {
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!”)
suspend fun work(){
val startTime = System.currentTimeMillis()
var nextPrintTime = startTime
var i = 0
while (i < 5) {
yield()
// print a message twice a second
if (System.currentTimeMillis() >= nextPrintTime) {
println("Hello ${i++}")
nextPrintTime += 500L
}
}
}
suspendCancellableCoroutine 与 invokeOnCancellation
如果你用suspendCoroutine方法将回调转为协程,那么最好改用suspendCancellableCoroutine。可以通过continuation.invokeOnCancellation来完成在取消阶段要进行的工作。例如
suspend fun work() {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
//清除工作
println("clean up done")
}
}
}
为了实现结构式并发并确保不执行不必要的工作,你要确保你的代码可取消。
使用Jetpack中定义的CoroutineScopes:viewmodelScope或lifecycleScope会在scope完成时取消他们的工作-也就是在Activity、fragment、lifecycle完成时。如果你创建自己的CoroutineScopes,确保它与job关联,并可以在需要的时候取消。
后记
下一节学习不应该被取消的工作
网友评论