CoroutineContext 解析

99 阅读5分钟

一、CoroutineContext继承关系

  • CoroutineScope接口
    • 唯一成员变量CoroutineContext
      • 子类Element接口
        • Job
        • CoroutineDispatcher
        • CoroutineName
        • CoroutineExceptionHandler

1.1、接口设计

CoroutineContext 的 API 设计和 Map 十分类似:

public interface CoroutineContext {
    public operator fun <E : Element> get(key: Key<E>): E?
    public operator fun plus(context: CoroutineContext): CoroutineContext {}
    public fun minusKey(key: Key<*>): CoroutineContext
    public fun <R> fold(initial: R, operation: (R, Element) -> R): R
    public interface Key<E : Element>
}

a611d29c307f953ebb099554a06a5d26.png

1.2、操作符重载

@ExperimentalStdlibApi
fun main() = runBlocking {
    val job = Job()
    val dispatcher: ExecutorCoroutineDispatcher = Executors.newSingleThreadExecutor {
        Thread(it, "MySingleThread").apply { isDaemon = true }
    }.asCoroutineDispatcher()

    val scope = CoroutineScope(job + dispatcher) // 操作符重载
    scope.launch {
        log(scope.coroutineContext[Job] === job) // 操作符重载
        log(scope.coroutineContext[CoroutineDispatcher] === dispatcher)
        log(scope.coroutineContext.get(CoroutineDispatcher) === dispatcher)
        log(coroutineContext[ExecutorCoroutineDispatcher] === dispatcher)
    }
    delay(500L)
}

fun log(text: Any) = println("$text - ${Thread.currentThread().name}".trimIndent())

在上面的代码中,我们:

  • 使用了 job + dispatcher 这样的方式,创建 CoroutineScope
  • 使用了 coroutineContext[XX] 这样的方式,访问当前协程所对应的 XX

代码之所以这么写,是因为 CoroutineContext 的 plus/get 方法支持操作符重载:

public operator fun plus(context: CoroutineContext): CoroutineContext
public operator fun <E : Element> get(key: Key<E>): E?
  • operator 修饰 plus() 方法后,就可以用 + 来重载这个方法
    • 比如,集合之间的合并操作:list3 = list1 + list2、map3 = map1 + map2
  • operator 修饰 get() 方法后,就可以用 [] 来重载这个方法
    • 比如,以数组下标的方式访问集合的元素:list[0]map[key]

如果 plus/get 方法声明中去掉了关键字 operator,就只能使用下面的方式了:

job.plus(dispatcher) // 或 dispatcher.plus(job)
scope.coroutineContext.get(CoroutineDispatcher)

Kotlin 中的集合与数组的访问方式,之所以可以保持一致,就是依赖于操作符重载。实际上,Kotlin 官方的源代码当中大量使用了操作符重载来简化代码逻辑,而 CoroutineContext 就是一个最典型的例子。

二、Job

基本上每启动一个协程就会产生对应的Job,例如

lifecycleScope.launch {
}

launch返回的就是一个Job,它可以用来管理协程。

CoroutineContext中的Job和父级上下文中的Job永远不会是同一个实例,因为一个新的coroutine总是得到一个Job的新实例。

一个Job中可以关联多个子Job,同时它也提供了通过外部传入parent的实现。

public fun Job(parent: Job? = null): Job = JobImpl(parent)

这个很好理解,当传入parent时,此时的Job将会作为parent的子Job

20c322b27709f5082285c8cfcafbdd6f.png

提供了六种状态来表示协程的运行状态。

  1. New: 创建
  2. Active: 运行
  3. Completing: 已经完成等待自身的子协程
  4. Completed: 完成
  5. Cancelling: 正在进行取消或者失败
  6. Cancelled: 取消或失败

这六种状态Job对外暴露了三种状态,它们随时可以通过Job来获取

public val isActive: Boolean
public val isCompleted: Boolean
public val isCancelled: Boolean

所以如果你需要自己来手动管理协程,可以通过下面的方式来判断当前协程是否在运行。

while (job.isActive) {
// 协程运行中            
}

一般来说,协程创建的时候就处在Active状态,但也有特例。

例如我们通过launch启动协程的时候可以传递一个start参数

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

如果这个start传递的是CoroutineStart.LAZY,那么它将处于New状态。可以通过调用start或者join来唤起协程进入Active状态。

                                        wait children
  +-----+ start  +--------+ complete   +-------------+  finish  +-----------+
  | New | -----> | Active | ---------> | Completing  | -------> | Completed |
  +-----+        +--------+            +-------------+          +-----------+
                   |  cancel / fail       |
                   |     +----------------+
                   |     |
                   V     V
               +------------+                           finish  +-----------+
               | Cancelling | --------------------------------> | Cancelled |
               +------------+                                   +-----------+

上面已经提及到一个Job可以有多个子Job,所以一个Job的完成都必须等待它内部所有的子Job完成;对应的cancel也是一样的。

默认情况下,如果内部的子Job发生异常,那么它对应的parent Job与它相关连的其它子Job都将取消运行。俗称连锁反应。

我们也可以改变这种默认机制,Kotlin提供了SupervisorJob来改变这种机制。这种情况还是很常见的,例如用协程请求两个接口,但并不想因为其中一个接口失败导致另外的接口也不请求,这时就可以使用SupervisorJob来改变协程的这种默认机制。

使用很简单,在我们创建CoroutineContext的时候加入SupervisorJob即可。例如在上面提到过的lifecycleScope,内部就使用到了SupervisorJob

val newScope =  CoroutineScope(Dispatchers.IO + SupervisorJob())
  • 如果有些任务你并不想被手动取消,可以使用NonCancellable作为任务的CoroutineContext

  • 如果需要Job获取协程的返回结果,可以通过Deferred来实现,它是Job的一个子类,所以也拥有Job所用功能。同时额外提供await方法来等待协程结果的返回。

  • Deferred可以通过CoroutineScope.async创建。

三、CoroutineDispatcher

public actual object Dispatchers {
    @JvmStatic public val IO: CoroutineDispatcher = DefaultIoScheduler
    @JvmStatic public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
    @JvmStatic public actual val Default: CoroutineDispatcher = DefaultScheduler
    @JvmStatic public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
}
  • Dispatchers.Main:只在 Android、Swing 之类的 UI 平台才有意义,在普通的 JVM 工程中是无法直接使用的
  • Dispatchers.Unconfined:无限制,当前协程可能运行在任意线程之上
  • Dispatchers.Default:用于 CPU 密集型任务的线程池,线程个数与 CPU 核心数量一致,最小为 2
  • Dispatchers.IO:用于 IO 密集型任务的线程池,线程数量一般比较多,可通过参数 kotlinx.coroutines.io.parallelism 配置

1、IO密集型任务

一般来说:文件读写、DB读写、网络请求等

2、CPU密集型任务

一般来说:计算型代码、Bitmap转换、Gson转换等

四、CoroutineName

用于指定协程的名称

五、CoroutineExceptionHandler

主要负责处理协程当中的异常

public interface CoroutineExceptionHandler : CoroutineContext.Element {
    public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>
    public fun handleException(context: CoroutineContext, exception: Throwable)
}

如果我们要自定义异常处理器,只需要实现 handleException() 方法即可:

CoroutineScope(Dispatchers.IO + SupervisorJob() + CoroutineName("aaa")
                        + CoroutineExceptionHandler { coroutineContext, throwable ->

               })