9

掌握Kotlin Coroutine之 Context&Dispatcher

 3 years ago
source link: http://blog.chengyunfeng.com/?p=1088
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

掌握Kotlin Coroutine之 Context&Dispatcher

作者: rain 分类: 移动 发布时间: 2019-03-15 21:33 6 0条评论

在前面介绍的 coroutine builder 函数中,都需要一个 CoroutineContext 参数。CoroutineContext 是很重要的一部分内容。

CoroutineContext 包含了一些用户定义的数据集合,这些数据和当前的 Coroutine 关联。CoroutineContext 和线程的 Thread-local 变量概念类似,区别在于 Thread-local 是可以被修改的而 CoroutineContext 是不可变(immutable)的。由于 CoroutineContext 是非常轻量级的实现,如果遇到 CoroutineContext 需要变化的时候, 只需要使用新的 context 重新创建一个 Coroutine 就可以了。

CoroutineContext 是一个被索引的 Element set 集合,里面的每个元素(Element)都有一个唯一的 Key。定义为一个 set 和 map 的混合,这样里面的每个元素都和 map 一样有个对应的 key,而每个 key 又像 set 一样直接和这个元素关联。

CoroutineContext 有两个非常重要的元素 — Job 和 Dispatcher,Job 是当前的 Coroutine 实例而 Dispatcher 决定了当前 Coroutine 执行的线程。Job 对象前面已经介绍过了,Dispatcher 将会在下面介绍。

先来看看 CoroutineContext 接口的定义:

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

CoroutineContext 定义了四个核心的操作:

  • 操作符(Operator)get 可以通过 key 来获取这个 Element。由于这是一个 get 操作符,所以可以像访问 map 中的元素一样使用 context[key] 这种中括号的形式来访问。
  • 函数 foldCollection.fold 扩展函数类似,提供便利当前 context 中所有 Element 的能力。
  • 操作符 plusSet.plus 扩展函数类似,返回一个新的 context 对象,新的对象里面包含了两个里面的所有 Element,如果遇到重复的(Key 一样的),那么用+号右边的 Element 替代左边的。
  • 函数 minusKey 返回删除一个 Element 的 context。

通过上面这些函数,context 可以很方便的组合使用,比如一个库定义了一个用来保存已经登录用户 id 的 auth Element,而另外一个库定义了一个包含一些执行信息的 threadPool Element, 可以通过 + 号来把这两个 context 组合一起使用:launch(auth + threadPool) {...},这样代码看起来更加直观。

标准库中包含了一个空的啥功能都没有的实现 EmptyCoroutineContext。一般继承 AbstractCoroutineContextElement 这个类来实现自定义的 context。

控制 Coroutine 的执行线程是非常重要的一个功能,而这个功能是通过 CoroutineDispatcher 这个 context 接口实现的。

CoroutineDispatcher

CoroutineDispatcher 定义了 Coroutine 执行的线程。CoroutineDispatcher 可以限定 Coroutine 在某一个线程执行、也可以分配到一个线程池来执行、也可以不限制其执行的线程。

CoroutineDispatcher 是一个抽象类,所有 dispatcher 都应该继承这个类来实现对应的功能。标准库中提供了下面几个常用的实现:

  • Dispatchers.Default — 如果创建 Coroutine 的时候没有指定 dispatcher,则一般默认使用这个作为默认值。Default dispatcher 使用一个共享的后台线程池来运行里面的任务。
  • Dispatchers.IO — 顾名思义这是用来执行阻塞 IO 操作的,也是用一个共享的线程池来执行里面的任务。根据同时运行的任务数量,在需要的时候会创建额外的线程,当任务执行完毕后会释放不需要的线程。通过系统 property kotlinx.coroutines.io.parallelism 可以配置最多可以创建多少线程,在 Android 环境中我们一般不需要做任何额外配置。
  • Dispatchers.Unconfined — 立刻在启动 Coroutine 的线程开始执行该 Coroutine直到遇到第一个 suspension point。也就是说,coroutine builder 函数在遇到第一个 suspension point 的时候才会返回。而 Coroutine 恢复的线程取决于 suspension function 所在的线程。 一般而言我们不使用 Unconfined
  • 通过 newSingleThreadContextnewFixedThreadPoolContext 函数可以创建在私有的线程池中运行的 Dispatcher。由于创建线程比较消耗系统资源,所以对于临时创建的线程池在使用完毕后需要通过 close 函数来关闭线程池并释放资源。
  • 通过 asCoroutineDispatcher 扩展函数可以把 Java 的 Executor 对象转换为一个 Dispatcher 使用。
  • Dispatchers.Main — 是在 Android 的 UI 线程执行。

由于子Coroutine 会继承父Coroutine 的 context,所以为了方便使用,我们一般会在 父Coroutine 上设定一个 Dispatcher,然后所有 子Coroutine 自动使用这个 Dispatcher,比如在前面的示例中,我们在 Activity 上实现 CoroutineScope:

class ScopedActivity : Activity(), CoroutineScope by MainScope() {
  // ......

上面 mainScope 的实现使用的是 Main Dispatcher,所以我们在 Activity 中创建的 Coroutine 都默认使用 Main Dispatcher 了:

public fun MainScope(): CoroutineScope =
  ContextScope(SupervisorJob() + Dispatchers.Main)

withContext() 函数

如果需要在 Coroutine 中创建一个不同 context 的子Coroutine,则可以使用 withContext() 这个函数来实现。比如:

Kotlin
suspend fun background() {
    withContext(BG) {
        Thread.sleep(3000)
        Log.d(TAG, "background() called ${Thread.currentThread().name}")

自定义 CoroutineContext

由于 CoroutineContext 是用户设置的一个 Coroutine 中的数据集合,所以自定义 Context 会是一个普遍的需求。 通过继承 AbstractCoroutineContextElement 抽象类实现自定义的 Context 是非常简单的,比如下面是一个用来保存登录用户 id 的 Context 实现:

Kotlin
data class AuthUser(val id: String) : AbstractCoroutineContextElement(AuthUser) {
    companion object Key : CoroutineContext.Key<AuthUser>
    // 可以不用重载 toString 函数
    override fun toString(): String = "Auth user id($id)"

上面的 AuthUser 类定义了一个变量 id 用来保存用户登录信息,然后重载了 toString 函数来打印用户 id,这样有助于调试的时候查看信息。除了 id 属性以外,最重要的就是该Context的 Key 实现了。 Key 在 CoroutineContext 的定义中只是一个接口,key 接口没有任何函数定义,所以 Key 接口只是作为一个 CoroutineContext 的索引使用,并且是全局唯一的,所以在每个 CoroutineContext 的实现中 Key 都是使用 companion object 来实现的。 这样定义可以保证在 Context 中每个类型的 Context 都是唯一的,并且访问 Context 的语法更加简洁。

比如要获取 context 中的 AuthUser 数据,正常情况下需要通过下面的方式来获取:

coroutineContext.get(AuthUser.Key)

而由于 get 函数是个特殊的操作符,所以可以用 [] 语法来访问,这样就变成了:

coroutineContext[AuthUser.Key]

而由于 Key 对象是 AuthUser 的 companion object,所以这个 Key 也是可以省略的,这样就进一步简化成了:

coroutineContext[AuthUser]

关于 companion object 请参考其 API 文档。

自定义 Dispatcher

CoroutineDispatcher 也是一种特殊的 Context,但是由于 CoroutineDispatcher 是用来控制 Coroutine 执行线程的,所以其实现原理比较复杂,而在一般的开发过程中我们只需要使用标准库定义的几种 Dispatchers 就够用了,不会涉及到需要自定义 Dispatcher 的情况,所以关于自定义 Dispatcher 就不做介绍了。

了解了 CoroutineContext 和 Dispatcher,在项目中就可以开始使用 Coroutine 了。使用不同的 Dispatcher 在后台线程执行一些耗时操作,然后当执行完毕后在 Main(UI)线程更新 UI。如果有一些参数需要传递到 Coroutine 中,可以通过自定义 Context 的方式来实现。 下面是一些参考文档的链接:

Conpanion Object API 文档:https://kotlinlang.org/docs/reference/object-declarations.html#companion-objects

CoroutineContext API 文档:https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/-coroutine-context/index.html

Collection.fold API 文档:https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/fold.html

Set.plus API 文档:https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/plus.html

CoroutineDispatcher API 文档:https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html

withContext() 函数 API 文档:https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK