Back
Featured image of post Kotlin Coroutine 协程 - 05 协程上下文

Kotlin Coroutine 协程 - 05 协程上下文

协程中的上下文系统

本文是对 Kotlin Coroutines by Tutorials By Filip Babić and Nishant Srivastava 书本内容的转述和笔记书本内容是受原版权保护的内容

上一篇:Kotlin Coroutine 协程 - 04 Async / Await

Chapter 5: 协程上下文

导言没有什么意义,删了(

Contextualizing Coroutines 协程的上下文化

每个协程都与一个 CoroutineContext 相联系。上下文是对一组 CoroutineContext.Elements 的封装,每个元素都描述了建立和形成一个协程的重要部分,比如,异常的传播方式,执行流程的导航,或者只是一般的生命周期。

这些元素分别是:

  • Job:作业,一个可取消的工作片段,它有一个定义的生命周期。

  • ContinuationInterceptor:一种机制,用于监听一个协程中的「延续」,并拦截「延续」的恢复。

  • CoroutineExceptionHandler:一种结构,用于处理协程中的异常。

因此,当你运行 launch 时,你可以传递给它一个你自己选择的上下文。上下文定义了哪些元素将被纳入拼图。如果你传入另一个同样实现了 CoroutineContext.ElementJob,你将定义新的协程的父级是什么。因此,如果父 Job 完成了,它将通知其所有的子 Job,包括新创建的协程。

然后,如果你传入一个异常处理程序,即另一个 CoroutineContext.Element,你就给了协程一种方法,在发生坏事时处理错误。

最后,你可以传入一个 ContinuationInterceptor。这些结构体通过决定那些由协程驱动的函数应该在哪个线程上操作,以及如何分配工作,来控制每个由协程驱动的函数的流程。

问题是,你不会想提供一个完整的实现来手动处理「延续」。如果你想让别的东西为你做这一部分,同时也是 CoroutineContext.Element,你必须提供一个协程调度器。你之前已经用过一些了,比如 Dispatchers.Default。因此,理解 ContinuationInterceptor 用法的关键是通过学习什么是真正的调度器,这一点你将在「第 6 章:上下文切换和调度」中进行。现在,你将专注于组合和提供 CoroutineContexts

Using CoroutineContext 使用协程上下文

尽管你还没有太深入地了解它,但你已经广泛地使用了 CoroutineContext。每次你从一个 CoroutineScope 创建一个协程时,你都会把这个 Scope 的 CoroutineContext 传给构建者。以下面的片段为例:

GlobalScope.launch {
  println("In a coroutine")
}

你没有看到,但在这个简单的代码片段中,围绕 CoroutineContext 做了一些工作。再一次,如果你看一下启动的定义,这就是你能看到的。

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

你可以看到,默认的上下文是 EmptyCoroutineContext。这意味着它将使用最默认的行为──没有特殊的生命周期,也没有来自协程内部的异常处理,最重要的是──没有自定义线程。接下来,launch 会调用 newCoroutineContext(context),为该协程建立一个完整的上下文。下面的代码有点复杂,但本质上,如果上下文是完全空的,它就会向它添加 Dispatchers.Default,增加默认的后台 Worker 线程。

因此,即使你没有看到,API 本身使用上下文来实现至少是默认的行为。但是你应该总是努力明确地、清晰地提供你想要发生的事情。

组合上下文可以产生非常强大的机制,所以让我们看看它是怎么回事。

Combing Different Contexts 组合不同的上下文

CoroutineContext 的另一个有趣的方面是能够对它们进行组合,以此组合它们的功能。使用 +plus 操作符,你可以从两者的组合中创建一个新的 CoroutineContext。因为你知道每个协程是由像是流程的「延续」拦截器、错误的异常处理程序和带生命周期的 Job 等对象组成的,所以必须有一种方法来创建一个具有所有这些对象的新协程。这就是「总结上下文」的用武之地。你可以像这样简单地做:

fun main() {
  val defaultDispatcher = Dispatchers.Default

  val coroutineErrorHandler = CoroutineExceptionHandler { context, error ->
    println("Problems with Coroutine: ${error}") // we just print the error here
  }

  val emptyParentJob = Job()

  val combinedContext = defaultDispatcher + coroutineErrorHandler + emptyParentJob

  GlobalScope.launch(context = combinedContext) {
    println(Thread.currentThread().name)
  }

  Thread.sleep(50)
}

上面的代码说明了,如何从 Dispatchers.Default 和错误处理程序中创建一个上下文。因此,你可以一次为一个上下文添加更多的功能,有效地建立起你的协程应该使用的所有功能──错误处理、线程和生命周期。因此,如果你为错误处理建立了一个复杂的 CoroutineContext,那么能够在你创建的每个协程中使用它将是非常酷的。协程的生命周期和其线程机制也是如此。

通过对 CoroutineContexts 进行加和,你结合了它们所有的 CoroutineContext.Elements,创造了一个它们功能的联盟。然而,有些事情是没有意义的,比如结合两个不同的 Dispatcher。这将意味着第二个调度器的线程将覆盖第一个调度器的线程。如果你试图这样做,编译器甚至会给你一条信息,说这是没有意义的。

Providing Contexts 提供上下文

当涉及到软件时,你通常希望以一种抽象化层间通信的方式来构建它。对于线程来说,抽象出不同线程之间的切换方式是非常有用的。你可以通过附加一个 线程提供者(Thread Provider) 来抽象出这一点,提供主线程和后台线程。这与协程没有什么不同!因为线程机制被抽象出来了。由于线程机制是通过CoroutineContext 对象和它们各自的 CoroutineDispatcher 实例抽象出来的,你可以建立一个提供者,用来委托你在每次构建协程时使用哪个上下文。通常,这些提供者有一个声明的接口,它给你提供了主线程和后台线程或调度器,因为这在有用户界面的应用程序中是很重要的。

让我们看看你是如何构建这样一个提供者的。

Building the ContextProvider 建立上下文

你已经了解了哪些 CoroutineContext 对象存在,以及它们的行为是什么。为了建立提供者,你首先要定义一个接口,它提供了一个通用的上下文,你将在上面运行一些费时的操作。注意,这个提供者接口不是协程的一部分,但将帮助我们抽象出主上下文。这个接口看起来是这样的:

interface CoroutineContextProvider {

  fun context(): CoroutineContext
}

这样,你可以建立许多不同的 CoroutineContextProviders,每个都是为了你心中的特定用例。在实现中,你可以将所需的 CoroutineContext 传递给构造函数,在工厂函数中抽象出这些信息,或者你的依赖注入图。

class CoroutineContextProviderImpl(
    private val context: CoroutineContext
) : CoroutineContextProvider {

  override fun context(): CoroutineContext = context
}

这样一来,无论什么时候你在构建协程,你都可以使用 CoroutineContextProvider

GlobalScope.launch(context = provider.context()) { }

正因为如此,你能够依赖抽象的上下文提供者,而不是手动编写你使用的所有上下文。此外,当你构建提供者时,你可以传入任何你想要的上下文。要创建这样一个提供者,你可以这样

val backgroundContextProvider = 
  CoroutineContextProviderImpl(Dispatchers.Default)

进一步,我们可以让上下文提供者不仅提供基于线程的上下文,还提供错误处理上下文和生命周期相关的上下文。或者几乎是这些协程上下文元素的任何组合。如果你想抽象出你经常使用的上下文,这就非常有用。此外,它可以帮助你进行测试,你会在「第15章:测试协程」中看到。

下一篇:Kotlin Coroutine 协程 - 06 上下文切换与协程调度器

comments powered by Disqus