Kotlin 协程源码阅读笔记 ------ CoroutineContext
如果你还了解过 Kotlin
协程,我建议你先了解一下 Kotlin
协程再来阅读本篇文章,不然你可能会看不懂本篇文章。
如果我们需要开启一个协程,首先需要构建一个 CoroutineScope
对象,然后通过这个对象再调用 launch()
方法就启动了一个协程,而 CortouineScope
只是一个简单的接口,需要实现类提供 CoroutineContext
对象,哈哈,终于见到我们本篇文章的主题了😂,通常我们会传递一个 CoroutineDispacher
对象作为默认的 CoroutineContext
,如果是你需要让协程的方法在主线程调度,就使用 Dispatchers.Main
,如果需要协程的方法在非主线程调用,就使用 Dispatchers.Default
。当然如果你不需要任何默认的 CoroutineContext
,可以指定为 EmptyCoroutineContext
。上面提到的 CoroutineDispatcher
(Dispatchers.Main
和 Dispatchers.Default
) 与 EmptyCoroutineContext
他们也都是属于 CoroutineContext
,这里简单介绍一下常见的 CoroutineContext
:
EmptyCoroutineContext
表示空的CoroutineContext
,他没有任何的功能,只是一个标志作用,表示没有CoroutineContext
。CoroutineDispatcher
控制协程运行调度相关的工作,协程的方法在具体的哪个线程执行也由它控制,常见的CoroutineDispatcher
有Dispatchers.Main
和Dispatchers.Default
。Job
表示协程任务的状态,当协程执行时会添加一个Job
,当协程开启一个子协程时又会开启一个Job
,子协程的Job
就是作为父协程的Child
,反之就是Parent
。当父协程的Job
被取消时,所有的子协程也都会被取消。我们在构建CoroutineScope
对象时,系统会为我们添加一个默认Job
,通过该CoroutineScope
启动的协程的Job
都是CoroutineScope
的子Job
,如果想要停止所有的CoroutineScope
启动的协程时,可以通过调用CoroutineScope#cancel()
方法,在这个方法里面就是调用的Job
的cancel()
方法。CoroutineExceptionHandler
协程异常拦截器,我们可以自定义这个拦截器用来拦截协程中遇到的异常,不是有很多人遇到协程中的奇怪的崩溃吗?当遇到某些不需要处理的崩溃时,就可以用它过滤掉。CoroutineId
用来指定协程的Id
,会通过它来修改执行的线程的名字,这个是系统调用的。
上面介绍了一些系统的 CoroutineContext
,这些 CoroutineContext
在协程执行的过程中发挥着重要的作用,你自己也可以实现一些 CoroutineContext
。
Element 和 CombinedContext
我们上面提到的系统的 CoroutineContext
除了 EmptyCoroutineContext
以外,其他的都是实现了 Element
接口(它继承于 CoroutineContext
),Element
表示实现了特定某项功能的 CoroutineContext
,而 CombinedContext
就表示一堆 Element
组成的复合型 CoroutineContext
,比如通过 Job
与 CoroutineDispatcher
就能够组合成一个 CombinedContext
,也就是相当于一个 Element
的集合。
Element
Kotlin
public interface Element : CoroutineContext {
/**
* A key of this coroutine context element.
*/
public val key: Key<*>
public override operator fun <E : Element> get(key: Key<E>): E? =
@Suppress("UNCHECKED_CAST")
if (this.key == key) this as E else null
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(initial, this)
public override fun minusKey(key: Key<*>): CoroutineContext =
if (this.key == key) EmptyCoroutineContext else this
}
在 Element
中有一个非常重要的对象 Key
,它表示这种类型的 Key
的 Element
在 CombinedContext
中只有一个这种类型的 Element
。通常这个 Key
的实现都是一个 Kotlin
中的伴生对象(也就是一个单例对象),我这里以 ExecutorCoroutineDispatcher
中的 Key
来举一个例子:
Kotlin
@ExperimentalStdlibApi
public companion object Key : AbstractCoroutineContextKey<CoroutineDispatcher, ExecutorCoroutineDispatcher>(
CoroutineDispatcher,
{ it as? ExecutorCoroutineDispatcher })
在 Element#get()
方法中的实现就是通过判断 Key
是否是当前对象的 Key
,如果是就返回当前 Element
对象,如果不是就返回空。
fold()
方法在 Kotlin
中的很多集合类有这个方法,它是用来遍历所有的元素然后把他们组合成一个新的对象返回,Element#fold()
也是一样的功能,不过它只有一个元素。
Element#minusKey()
方法是用来移除某个 Key
的元素,Element
中就只有一个元素,如果传入的 Key
是当前 Element
的 Key
就返回 EmptyCoroutineContext
,反之就返回自己。
CombinedContext
Kotlin
@SinceKotlin("1.3")
internal class CombinedContext(
private val left: CoroutineContext,
private val element: Element
) : CoroutineContext, Serializable {
override fun <E : Element> get(key: Key<E>): E? {
var cur = this
// 遍历查找
while (true) {
// 如果当前的节点的 get() 返回不为空就表示这就是目标的 Element,直接返回
cur.element[key]?.let { return it }
// 下一个节点
val next = cur.left
// 如果下一个节点是 CombinedContext,继续遍历查找。
if (next is CombinedContext) {
cur = next
} else {
// 下一个节点不是 CombinedContext,直接调用 get() 方法返回。
return next[key]
}
}
}
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(left.fold(initial, operation), element)
public override fun minusKey(key: Key<*>): CoroutineContext {
// 如果当前 Element 的 key 就是需要移除的 Element,直接返回下一个节点
element[key]?.let { return left }
// 调用上一个节点的 minusKey 方法,返回值为移除后对象
val newLeft = left.minusKey(key)
return when {
// 新的和旧的没有改变表示没有找到对应的 key,直接返回当前对象。
newLeft === left -> this
// 新的下一个节点为空,直接返回 element
newLeft === EmptyCoroutineContext -> element
// 重新组合成一个新的 CombinedContext 对象。
else -> CombinedContext(newLeft, element)
}
}
// ...
}
CombinedContext
的实现可以理解为是一个 Element
的链表结构,其中 element
表示当前元素,left
表示指向下一个节点。
CombinedContext#get()
方法中依次查询每个节点的 Key
是否和 Element
对应,如果对应就直接返回。
CombinedContext#fold()
没什么好说的,依次调用每个 Element
的 fold()
方法最后得到组合后的对象。
CombinedContext#minusKey()
方法是用来移除某个 Key
的元素,返回值是被移除 Key
后新的 CoroutineContext
,它这里的实现不是通过直接删除当前对象的某个元素,而是返回一个新的删除某个元素后的新对象。这种思想非常重要,用新的组合来替代修改,在 Kotlin
协程的实现中有很多处都体现了这种思想,这种实现能够更容易地避免 Bug,而且可以逃避一些锁的操作。
组合两个 CoroutineContext
在上面一节了解到用来描述组合后的 CoroutineContext
,用的是 CombinedContext
这个对象,那么如何组合呢?
Kotlin
val context1 = Dispatchers.Default
val context2 = Job()
val combinedContext = context1 + context2
像以上代码一样直接用 +
符号就好了,如果你不知道 Kotlin
的符号重载和中坠函数,我建议你去找找相关的资料学习下。这个函数的实现是 CoroutineContext#plus()
方法:
Kotlin
public operator fun plus(context: CoroutineContext): CoroutineContext =
if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
// 遍历合并被加的 Context,合并时初始化的值为当前对象。
context.fold(this) { acc, element ->
// 移除参数中的 Element (也就是原来的 Element 与参数中的 Element有冲突,参数中的 Element 优先级高)
val removed = acc.minusKey(element.key)
if (removed === EmptyCoroutineContext) element else {
// make sure interceptor is always last in the context (and thus is fast to get when present)
// 以下的操作是始终让 ContinuationInterceptor 在链表的头部
val interceptor = removed[ContinuationInterceptor]
if (interceptor == null) CombinedContext(removed, element) else {
val left = removed.minusKey(ContinuationInterceptor)
if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
CombinedContext(CombinedContext(left, element), interceptor)
}
}
}
Element
出现冲突时,参数中的 Element
的优先级比当前对象的 Element
要高;始终会将 ContinuationInterceptor
放在链表的第一个元素,因为协程运行时获取这个元素的频率很高。
最后
CoroutineContext
中的链表实现方式很有意思,而且在 Kotlin
的实现中很多的地方都用了组合的思想来替代修改。很多时候 var
就是造成 BUG
的元凶,同时也会造成多线程编程困难。而 val
就能够在一定的程度上缓解这种问题。