【译】Koltin协程:First things first

原文:Coroutines: First things first
做者:Manuel Vivo
译者:Luckykelanjava

这一系列的博客文章深刻讨论了Kotlin协程中的取消和异常。为了不浪费内存和电池寿命,协程的取消相当重要;正确的异常处理是得到良好用户体验的关键。做为本系列其余两部分(第2部分:取消,第3部分:异常)的基础,这篇博客定义一些协程的核心概念,如协程做用域、Job和协程上下文等。android

协程做用域(CoroutineScope)

CoroutineScope 会跟踪使用launch async 建立的任何协程(这些是CoroutineScope 上的扩展函数),而且能够在任什么时候间点经过调用scope.cancel() 取消正在此上下文中进行的协程。
每当咱们想在咱们的APP中启动并控制一个协程的生命周期时,都应该建立一个 CoroutineScope 对象,在Android平台,已有KTX库提供了某些具备生命周期的类的 CoroutineScope ,如 viewModelScope lifecycleScope 。 在建立 CoroutineScope 时,它将一个 CoroutineContext 对象做为构造参数,你可使用以下代码建立新的协程做用域并使用它启动一个协程:并发

// Job and Dispatcher组成一个新的CoroutineScope
// 咱们稍后进行讨论
val scope = CoroutineScope(Job() + Dispatchers.Main)
val job = scope.launch {
    // new coroutine
}
复制代码

Job

Job是协程的句柄,对于您经过 launch async 启动的每一个协程,它都会返回一个Job实例,该实例惟一地标识了此协程并管理其生命周期。正如上面的代码,您还能够将Job对象传递给 CoroutineScope 来保持对此协程生命周期的控制。async

协程上下文(CoroutineContext)

CoroutineContext 是一组定义协程行为的元素。它由:函数

  • Job — 控制协程的生命周期。
  • 协程调度器( CoroutineDispatcher )— 将协程分配到适当的线程。
  • CoroutineName — 协程的名称,对调试颇有帮助。
  • CoroutineExceptionHandler — 处理未捕获的异常,将在本系列的第3部分中介绍。

当咱们建立一个新的协程时,它的协程上下文都有什么呢?咱们已经知道协程会返回一个新的Job 实例从而容许咱们控制它的生命周期,而其他的元素将今后协程的父元素(父协程或建立它的协程做用域)继承[^译者注]。
因为协程做用域能够建立协程,而且咱们能够在协程中建立更多的协程,所以会造成一个隐式的任务层次结构,在下面的代码中,除了使用协程做用域( CoroutineScope )建立一个新的协程外,还能够看看如何在协程中建立更多的协程:spa

val scope = CoroutineScope(Job() + Dispatchers.Main)
val job = scope.launch {
    // CoroutineScope做为父级的新协程
    val result = async {
        // 以协程做为父级建立的新协程
    }.await()
}
复制代码

这种层次结构的根一般为最上层的协程做用域( CoroutineScope )。咱们能够把该层次结构可视化以下: 线程

协程在任务层次结构中执行。父级能够是CoroutineScope或另外一个协程

译者注:这种任务的层次结构就是Koltin协程引觉得傲的结构化并发 — 父协程能够控制和限制子线程的生命周期,子协程会继承父协程的协程上下文 调试

Job的生命周期

Job 经历如下一系列状态:新建、活动、正在完成、已完成、正在取消和已取消状态。虽然咱们没法访问状态自己,但能够访问Job 的属性: isActive isCancelled isCompleted code

若是协程处于活动状态,则协程失败或调用 job.cancel()方法将使Job处于取消状态( isActive = false, iscancel = true )。一旦全部的子协程完成了他们的工做,协程将进入取消状态而且 isCompleted = true

关于父级CoroutineContext的解释

在协程的任务层次结构中,每一个协程都有一个父级,能够是一个协程做用域,也能够是另外一个协程。然而,协程继承的父级协程上下文可能不一样于父级自己的上下文,由于协程上下文是基于这个公式计算的cdn

父级上下文 = 默认值 + 继承的上下文 + 参数

  • 某些元素具备默认值,如 Dispatchers.Default 是协程调度器( CoroutineDispatcher )的默认值,“coroutine”是 CoroutineName 的默认值。
  • 协程会继承建立它的协程做用域或协程的 CoroutineContext
  • 在协程构建器中传递的参数会优先于继承的 CoroutineContext 中的元素。

注意: CoroutineContext 可使用+运算符组合,因为 CoroutineContext 是一组元素,所以会将加号右边的元素覆盖左侧相同的元素以建立一个新的 CoroutineContext ,如 (Dispatchers.Main, “name”) + (Dispatchers.IO) = (Dispatchers.IO, “name”)

由scope建立的协程,在它的CoroutineContext中至少拥有这些元素,CoroutineName的值为灰色缘由是它来自默认值

译者注:根据CoroutineContext的源码,CoroutineContext在内部使用键值对来维护元素,比较神奇的操做是,这些键值对的键对应着值类型的伴生对象,值为这些类型的实例,因此同一类型的元素在同一个CoroutineContext中惟一。

如今咱们知道了一个新的协程的父级协程上下文是什么,那么它实际的协程上下文将是:

新协程的上下文 = 父级CoroutineContext + Job()

若是使用上图的scope建立一个新协程,就像这样:

val job = scope.launch(Dispatchers.IO){ 
    //新协程
}
复制代码

那么这个新协程的协程上下文和父级协程上下文将是什么呢?请参见下图的答案!

父级上下文中的Job永远也不会与新协程的上下文的Job是同一实例,由于新协程始终会返回一个新的Job实例
咱们看到父级上下文中有 Dispatchers.IO ,而不是声明 scope 时咱们传入的 Dispatchers.Main ,由于它被协程构建器 launch 的参数覆盖了。另外,请注意父级上下文中的Job实例是scope对象的Job,而新协程的实际Job实例是被从新分配的(绿色)。
咱们将在本系列的第三部分看到,协程上下文中有一个叫 SupervisorJob Job 实现, SupervisorJob 改变了 CoroutineScope 处理异常的方式。