我将翻译四篇介绍协程的 取消 和 异常处理 相关的文章,四篇文章是层层递进的关系。翻译过程中我将尽量忠实于原文。当然,由于水平有限,不能保证完全的翻译正确。如果您发现翻译有错误之处,欢迎在评论里指出。我也将贴出每篇翻译的原文。
- 第一篇:《协程:第一件事》(原文: Coroutines: first things first)
- 第二篇:《协程的取消》(原文:Cancellation in coroutines)
- 第三篇:《协程的异常》(原文:Exceptions in coroutines)
- 第四篇:《不应该被取消的协程和模式》(原文:Coroutines & Patterns for work that shouldn’t be cancelled)
那么,这是第一篇。
文章目录
- CoroutineScope
- Job
- CoroutineContext
- Job的生命周期
- Parent CoroutineContext
本系列的博文将深入讨论协程的 取消(cancellation)和 异常(exception)。取消是很重要的,可以避免做不必要的工作,以避免浪费内存和电量;适当的异常处理是用户体验的关键所在。作为该系列其它篇章的基础,定义好协程的一些核心概念非常重要,比如 CoroutineScope,Job和 CoroutineContext,以便我们对这些概念有一致的认知。
如果您更喜欢视频,可以查看2019年Kotlin大会上的演讲视频:
KotlinConf 2019: Coroutines! Gotta catch 'em all! by Florina Muntenescu & Manuel Vivo
CoroutineScope
CoroutineScope能跟踪使用 launch或 async方法创建的任意协程(这两个方法是 CoroutineScope的扩展方法)。在任意时间点,调用 scope.cancel()方法都可以取消正在执行的协程。
无论何时,当你想在应用的特定层级启动一个协程并且控制它的生命周期,你就应该创建一个 CoroutineScope。在某些平台上,比如Android,就已经有 KTX库可以在特定的生命周期类中提供 CoroutineScope,比如 viewModelScope和 lifecycleScope。
创建一个 CoroutineScope需要 CoroutineContext作为它构造函数的参数。你可以通过下面的代码创建一个新的 scope和协程:
// Job 和 Dispatcher 被组合成一个 CoroutineContext,我们后面会讨论到 val scope = CoroutineScope(Job() + Dispatchers.Main) val job = scope.launch { //新的协程 }
Job
Job是协程的一个句柄。所有你通过 launch或 async创建的协程都会返回一个 Job实例,它可以标识该协程,并且能控制协程的生命周期。在上面的代码中我们可以看到,你可以传递一个 Job给 CoroutineScope来保持对其生命周期的控制。
CoroutineContext
CoroutineScope是由一系列元素组合而成的,用来定义协程的行为。它的组成元素包括:
- Job —— 控制协程的生命周期
- CoroutineDispatcher —— 线程调度器
- CoroutineName —— 协程的名字,主要用于调试
- CoroutineExceptionHandler —— 处理未被捕获的异常,我们将在该系列的第三篇中讲到它
一个新协程的 CoroutineContext是什么样子的?我们已经知道它将创建一个新的 Job实例,用于控制其生命周期。它其余的元素将从它的父节点(创建它的另一个协程,或 CoroutineScope)继承而来。
由于 CoroutineScope可以创建协程,并且你可以在一个协程内部创建更多的协程,所以就构建了一个隐式的任务层次结构。在下面的代码片段中,使用 CoroutineScope,除了创建一个新的协程之外,还可以看到在协程内部如何创建更多的协程:
val scope = CoroutineScope(Job() + Dispatchers.Main) val job = scope.launch { //scope作为父节点的新协程 val result = async { //由launch创建的协程作为父节点的新协程 }.await() }
该层次结构的根节点通常是 CoroutineScope,我们把它可视化如下:
(协程在一个任务层次结构中执行,其父节点可以是一个 CoroutineScope,也可以是另一个协程)
Job的生命周期
一个 Job可以经历一系列状态:New,Active,Completing,Completed,Cancelling以及 Cancelled。我们无法直接访问 Job的状态,但可以访问它的属性:isActive,isCancelled以及 isCompleted。它们之间的关系如下图:
如果一个协程处于 Active的状态,当协程发生异常或调用了 job.cancel()方法,其 Job的状态将变为 Cancelling(isActive = false, isCancelled = true)。一旦所有子协程的任务都已完成,该协程就会走到 Cancelled状态,并且 isCompleted = true。
Parent CoroutineContext
在任务层次结构中,每一个协程都有一个父节点,父节点可能是一个 CoroutineScope,也可能是另一个协程。然而,子协程的 parent CoroutineContext和其父节点的 CoroutineContext可能不一样,它由以下公式计算而成:
Parent context = 默认值 + 继承的CoroutineContext + 传参
- 一些元素有默认的值,比如 CoroutineDispatcher的默认值是 Dispatchers.Default,CoroutineName的默认值是 "coroutine"。
- 继承的 CoroutineContext就是创建了该协程的 CoroutineScope 或 另一个协程 的 CoroutineContext。
- 传给协程构建器的参数优先于来自继承的参数。
注意:CoroutineContext 可以使用 + *** 作符来组合。由于 CoroutineContext是元素的组合,一个新的 CoroutineContext被创建的时候,加号 *** 作符右侧的元素会覆盖 *** 作符左侧的元素。
比如: (Dispatchers.Main, "name") + (Dispatchers.IO) = (Dispatchers.IO, "name")
(每一个由图片中的这个 CoroutineScope启动的协程,其 CoroutineContext里都至少有这些元素。CoroutineName是灰色的因为它使用了默认值。)
现在我们知道了什么是一个新协程的 parent CoroutineContext,而新协程自身的 CoroutineContext将是:
新协程的CoroutineContext = parent CoroutineContext + Job()
如果我们用上图中的 CoroutineScope创建一个新的协程,如下:
val job = scope.launch(Dispatchers.IO) { //新协程 }
那么它的 parent CoroutineContext和它自身的 CoroutineContext是怎样的呢?从下图中可以看到答案!
(协程 CoroutineContext中的 Job和其 parent CoroutineContext中的 Job永远不会是同一个实例,一个新的协程总会获取一个新的 Job实例)
新建协程的 parent CoroutineContext使用了 Dispatchers.IO,而不是 scope中的 CoroutineDispatcher,因为它被协程构建器中的参数覆盖了。并且,parent CoroutineContext中的 Job就是 scope的 Job实例(红色),新协程的 CoroutineContext中的 Job是一个新的实例(绿色)。
在该系列文章的第三篇中,我们将看到 CoroutineScope的 CoroutineContext中可以有一个不同的 Job实现叫做 SupervisorJob,它将改变 CoroutineScope处理异常的方式。因此,一个使用这种 scope创建的新协程将以 SupervisorJob作为 parent Job。然而,当一个协程的父节点是另一个协程时, parent Job的类型总是 Job(而非 SupervisorJob)。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)