关于android:协程中的取消和异常-取消操作详解

5次阅读

共计 5391 个字符,预计需要花费 14 分钟才能阅读完成。

在日常的开发中,咱们都晓得应该防止不必要的工作解决来节俭设施的内存空间和电量的应用——这一准则在协程中同样实用。您须要管制好协程的生命周期,在不须要应用的时候将它勾销,这也是结构化并发所提倡的,持续浏览本文来理解无关协程勾销的前因后果。

⚠️ 为了可能更好地了解本文所讲的内容,建议您首先浏览本系列中的第一篇文章: 协程中的勾销和异样 | 外围概念介绍。

调用 cancel 办法

当启动多个协程时,无论是追踪协程状态,还是独自勾销各个协程,都是件让人头疼的事件。不过,咱们能够通过间接勾销协程启动所波及的整个作用域 (scope) 来解决这个问题,因为这样能够勾销所有已创立的子协程。

// 假如咱们曾经定义了一个作用域

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

scope.cancel()

勾销作用域会勾销它的子协程

有时候,您兴许仅仅须要勾销其中某一个协程,比方用户输出了某个事件,作为回应要勾销某个进行中的工作。如下代码所示,调用 job1.cancel 会确保只会勾销跟 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 异样,您便能够应用这一机制来解决协程的勾销。无关如何执行此操作的更多信息,请参考上面的解决勾销的副作用一节。

在底层实现中,子协程会通过抛出异样的形式将勾销的状况告诉到它的父级。父协程通过传入的勾销起因来决定是否来解决该异样。如果子协程因为 CancellationException 而被勾销,对于它的父级来说是不须要进行其余额定操作的。

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

如果您应用的是 androidx KTX 库的话,在大部分状况下都不须要创立本人的作用域,所以也就不须要负责勾销它们。如果您是在 ViewModel 的作用域中进行操作,请应用 viewModelScope.viewModelScope:kotlinx.coroutines.CoroutineScope),或者如果在生命周期相干的作用域中启动协程,那就应该应用 [lifecycleScope](https://developer.android.goo…
)。viewModelScope 和 lifecycleScope 都是 CoroutineScope 对象,它们都会在适当的工夫点被勾销。例如,当 ViewModel 被革除时,在其作用域内启动的协程也会被一起勾销。

为什么协程解决的工作没有进行?

如果咱们仅是调用了 cancel 办法,并不意味着协程所解决的工作也会进行。如果您应用协程解决了一些绝对较为沉重的工作,比方读取多个文件,那么您的代码不会主动就进行此工作的进行。

让咱们举一个更简略的例子看看会产生什么。假如咱们须要应用协程来每秒打印两次 “Hello”。咱们先让协程运行一秒,而后将其勾销。其中一个版本实现如下所示:

咱们一步一步来看产生了什么。当调用 launch 办法时,咱们创立了一个沉闷 (active) 状态的协程。紧接着咱们让协程运行了 1,000 毫秒,打印进去的后果如下:

Hello 0
Hello 1
Hello 2

当 job.cancel 办法被调用后,咱们的协程转变为勾销中 (cancelling) 的状态。然而紧接着咱们发现 Hello 3 和 Hello 4 打印到了命令行中。当协程解决的工作完结后,协程又转变为了已勾销 (cancelled) 状态。

协程所解决的工作不会仅仅在调用 cancel 办法时就进行,相同,咱们须要批改代码来定期检查协程是否处于沉闷状态。

让您的协程能够被勾销

您须要确保所有应用协程解决工作的代码实现都是合作式的,也就是说它们都配合协程勾销做了解决,因而您能够在工作解决期间定期检查协程是否已被勾销,或者在解决耗时工作之前就查看以后协程是否已勾销。例如,如果您从磁盘中获取了多个文件,在开始读取文件内容之前,先查看协程是否被勾销了。相似这样的解决形式,您能够防止解决不必要的 CPU 密集型工作。

val job = launch {for(file in files) {
        // TODO 查看协程是否被勾销
        readFile(file)
    }
}

所有 kotlinx.coroutines 中的挂起函数 (withContext, delay 等) 都是可勾销的。如果您应用它们中的任一个函数,都不须要查看协程是否已勾销,而后进行工作执行,或是抛出 CancellationException 异样。然而,如果没有应用这些函数,为了让您的代码可能配合协程勾销,能够应用以下两种办法:

  • 查看 job.isActive 或者应用 ensureActive()
  • 应用 yield() 来让其余工作进行

查看 job 的沉闷状态

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

// 因为处于 launch 的代码块中,能够拜访到 job.isActive 属性
while (i < 5 && isActive)

这样意味着咱们的工作只会在协程处于沉闷的状态下执行。同样,这也意味着在 while 循环之外,咱们若还想解决别的行为,比方在 job 被勾销后打日志进去,那就能够查看 !isActive 而后再持续进行相应的解决。

Coroutine 的代码库中还提供了另一个很有用的办法 —— ensureActive(),它的实现如下:

fun Job.ensureActive(): Unit {if (!isActive) {throw getCancellationException()
    }
}

如果 job 处于非沉闷状态,这个办法会立刻抛出异样,咱们能够在 while 循环开始就应用这个办法。

while (i < 5) {ensureActive()
    …
}

通过应用 ensureActive 办法,您能够防止应用 if 语句来查看 isActive 状态,这样能够缩小样板代码的使用量,然而相应地也失去了解决相似于日志打印这种行为的灵活性。

应用 yield() 函数运行其余工作

如果要解决的工作属于 1) CPU 密集型,2) 可能会耗尽线程池资源,3) 须要在不向线程池中增加更多线程的前提下容许线程解决其余工作,那么请应用 yield()。如果 job 曾经实现,由 yield 所解决的首要任务将会是查看工作的实现状态,实现的话则间接通过抛出 CancellationException 来退出协程。yield 能够作为定期检查所调用的第一个函数,例如下面提到的 ensureActive() 办法。

Job.join ???? Deferred.await cancellation**

期待协程处理结果有两种办法: 来自 launch 的 job 能够调用 join 办法,由 async 返回的 Deferred (其中一种 job 类型) 能够调用 await 办法。

Job.join 会挂起协程,直到工作解决实现。与 job.cancel 一起应用时,会依照以下形式进行:

  • 如果您调用  job.cancel 之后再调用 job.join,那么协程会在工作解决实现之前始终处于挂起状态;
  • 在 job.join 之后调用 job.cancel 没有什么影响,因为 job 曾经实现了。

如果您关怀协程处理结果,那么应该应用 Deferred。当协程实现后,后果会由 Deferred.await 返回。Deferred 是 Job 的其中一种类型,它同样能够被勾销。

在已勾销的 deferred 上调用 await 会抛出 JobCancellationException 异样。

val deferred = async {…}

deferred.cancel()
val result = deferred.await() // 抛出 JobCancellationException 异样 

为什么会拿到这个异样呢?await 的角色是负责在协程处理结果进去之前始终将协程挂起,因为如果协程被勾销了那么协程就不会持续进行计算,也就不会有后果产生。因而,在协程勾销后调用 await 会抛出 JobCancellationException 异样: 因为 Job 已被勾销。

另一方面,如果您在 deferred.cancel 之后调用 deferred.await 不会有任何状况产生,因为协程曾经解决完结。

解决协程勾销的副作用

假如您要在协程勾销后执行某个特定的操作,比方敞开可能正在应用的资源,或者是针对勾销须要进行日志打印,又或者是执行其余的一些清理代码。咱们有好几种办法能够做到这一点:

查看 !isActive

如果您定期地进行 isActive 的查看,那么一旦您跳出 while 循环,就能够进行资源的清理。之前的代码能够更新至如下版本:

while (i < 5 && isActive) {if (…) {println(“Hello ${i++}”)
        nextPrintTime += 500L
    }
}
 
// 协程所解决的工作曾经实现,因而咱们能够做一些清理工作
println(“Clean up!”)

您能够查看 残缺版本。

所以当初,当协程不再处于沉闷状态,会退出 while 循环,就能够解决一些清理工作了。

Try catch finally

因为当协程被勾销后会抛出 CancellationException 异样,咱们能够将挂起的工作搁置于 try/catch 代码块中,而后在 finally 代码块中执行须要做的清理工作。

val job = launch {
   try {work()
   } catch (e: CancellationException){println(“Work cancelled!”)
    } finally {println(“Clean up!”)
    }
}

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

然而,一旦咱们须要执行的清理工作也挂起了,那上述代码就不可能持续工作了,因为一旦协程处于勾销中状态,它将不能再转为挂起 (suspend) 状态。您能够查看 残缺代码。

处于勾销中状态的协程不可能挂起

当协程被勾销后须要调用挂起函数,咱们须要将清理工作的代码搁置于 NonCancellable CoroutineContext 中。这样会挂起运行中的代码,并放弃协程的勾销中状态直到工作解决实现。

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!”)

您能够查看其 工作原理。

suspendCancellableCoroutine 和 invokeOnCancellation

如果您通过 suspendCoroutine 办法将回调转为协程,那么您更应该应用 suspendCancellableCoroutine 办法。能够应用 continuation.invokeOnCancellation 来执行勾销操作:

suspend fun work() {
   return suspendCancellableCoroutine { continuation ->
       continuation.invokeOnCancellation {// 解决清理工作}
   // 残余的实现代码
}

为了享受到结构化并发带来的益处,并确保咱们并没有进行多余的操作,那么须要保障代码是可被勾销的。

应用在 Jetpack: viewModelScope 或者 lifecycleScope 中定义的 CoroutineScopes,它们在 scope 实现后就会勾销它们解决的工作。如果要创立本人的 CoroutineScope,请确保将其与 job 绑定并在须要时调用 cancel。

协程代码的勾销须要是合作式的,因而请将代码更新为对协程的勾销操作以延后的形式进行查看,并防止不必要的操作。

当初,大家理解了本系列的第一局部 协程的一些基本概念、第二局部协程的勾销,在接下来的文章中,咱们将持续深入探讨学习第三局部异样解决,感兴趣的读者请持续关注咱们的更新。

正文完
 0