跳至主要內容

上下文「化」的协程

guodongAndroid大约 4 分钟

每一个协程都绑定一个 CoroutineContext。这个上下文是一个 CoroutineContext.Elements 集合的包装器,其中每个元素都描述了构建和形成协程的重要部分,以及异常传播和跳转执行流程的方式,或者仅仅是协程的生命周期。

这些元素是:

  • Job:一个有生命周期且可取消的任务。
  • ContinuationInterceptor:一个可以监听协程中的续体并拦截它的恢复流程的机制。
  • CoroutineExceptionHandler:一个可以处理协程中异常的处理器。

所以,当你调用 launch 时,你可以自己选择一个上下文传给它。这个上下文定义了哪些元素可以传入。如果你传入一个实现了 CoroutineContext.Elements 接口的 Job,意味着这个 Job 是所有新协程的父级。因此,如果这个父 Job 执行完成,它将通知它所以的子级,包含新创建的协程。

如果你传入另一个实现了 CoroutineContext.Elements 接口的异常处理器,意味着在协程中有异常发生时,可以有一个处理异常的方式。

最后,你也可以传入 ContinuationInterceptor 实例对象。这些对象通过确定应在哪个线程上运行以及应如何分配工作来控制每个基于协程函数的执行流程。

你肯定不想自己编写处理续体的完整实现。如果你想由其他的事物来实现,并且它又是 CoroutineContext.Elements,则必须提供 协程调度器

其实,之前你已经使用过它们了 —— 比如:Dispatchers.Default。所以,现在的关键是在「第七章:上下文切换和调度」中通过学习什么是调度器来理解 ContinuationInterceptor 的使用。

使用上下文

使用 IntelliJ 打开本章节的 starter 项目。

即使你还没有深入的了解 CoroutineContext,但是你已经经常使用它了。每次你通过 CoroutineScope 构建一个协程时,你已经将作用域的 CoroutineContext 传入构建器。比如以下代码片段:

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

虽然你看不到它,但对于上述简单的代码片段,CoroutineContext 已经完成了一些工作。让我们再次查看 launch 函数的定义,你将看到:

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

你将看到默认的上下文是 EmptyCoroutineContext。这基本上意味着它将使用默认的行为:

  • 没有特殊的生命周期
  • 最重要的是,在协程内没有异常处理
  • 没有自定义的线程

接下来,launch 调用 newCoroutineContext(context) 来为协程构建完整的上下文。下面的代码有点复杂,但本质上,如果上下文完全为空,它会向其中添加 Dispatchers.Default,从而添加默认的后台工作线程。

因此,即使你看不到它,launch 本身也使用默认上下文来至少实现默认行为。但你始终应该尽可能地提供上下文来明确且清楚的表示你想要做什么。

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

组合不同的上下文

协程上下文的另一个有趣的方面是能够把它们组合起来并且它们的功能也可以组合。使用 +/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 和异常处理器创建上下文的示例。因此,你可以一次性向上下文添加更多功能,从而有效地构建协程应使用的所有功能 —— 异常处理、线程和生命周期。

如果你运行上述代码,可能会看到如下输出:

DefaultDispatcher-worker-1
Process finished with exit code 0

尽管它只打印出了线程名称,但它背后有强大的机制在工作。

所以,如果你构建了如上所示的一个复杂 CoroutineContext,如果能够在你创建的每个协程中使用它,那就太棒了。协程的生命周期及其线程都会有相同的机制。

通过 CoroutineContexts 的相加组合,你通过合并所有的 CoroutineContext.Elements 创建了一个它们功能的结合体。然而,有些事情是没有意义的,比如:组合两个不同的调度器。这意味着第二个调度器将覆盖第一个调度器。如果你尝试这样做,编译器甚至会给你一条消息,说这样没有意义。