前面入门时讲过一个最简单的例子,通过 GlobalScope.launch { }
可以启动一个协程,GlobalScope
可以简单理解为协程构造者,它实际上是接口 CoroutineScope
的子类,那我们来看看它到底是什么,启动一个协程需要哪些关键要素。接下来我们讲讲协程相关的几个主要类,先混个脸熟,心里有个大体概念之后,再逐步深入。
1. CoroutineScope介绍
顾名思义“协程域”,只有它能创建协程,既然是创建者,同样它能管理它所创建的协程。该接口定义如下:
public interface CoroutineScope {
/**
* The context of this scope.
* Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
* Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
*
* By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
*/
public val coroutineContext: CoroutineContext
}
接口定义很简单,只包含一个叫 CoroutineContext
的参数,我们称之为协程上下文,那么这又是个什么鬼?我们应该在很多地方都见过名叫上下文的东西,例如在 Android 中一个 Activity 就是上下文 Context 的子类,由此可以类推 CoroutineContext 包含了协程运行时的一些信息,具体后面再逐步介绍。我们再看看 GlobalScope
的定义:
public object GlobalScope : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
原来 GlobalScope
是个类似 Java 中的单例类,它的协程上下文是个空上下文 EmptyCoroutineContext
。那么协程的启动方法是在哪里定义的呢,接口里我们好像没见到。原来协程的启动方法都是通过扩展函数来定义的,它的方法签名为:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
从方法定义中可以看到,协程的启动需要3个参数:context(协程上下文)、start(协程启动模式)、block(协程体),其中前2个参数都有默认值,我们例子中的代码其实只包含了协程体。协程上下文的概念很复杂,也特别难理解,我们可以将之类比为 Android 中的 Activity一样。协程体就像 Thread.run()
方法中的代码一样,协程的运行代码都应该写在里面,这个很容易理解。该方法会返回一个 Job
类型的对象,有趣的是 Job
也是继承自 CoroutineContext
,可以认为协程就是一个任务。
2. CoroutineStart(启动模式)介绍
CoroutineStart 是个枚举类,其定义如下:
public enum class CoroutineStart {
DEFAULT,
LAZY,
@ExperimentalCoroutinesApi
ATOMIC,
@ExperimentalCoroutinesApi
UNDISPATCHED;
}
共定义了4种启动模式,但是后2种还是带有实验性质的 Api,我们分别用代码来演示它们之间的区别。
2.1 DEFAULT
这是默认的启动模式,一旦 launch
方法调用后,立即开始调度协程的执行。这种模式有点像线程调用 Thread.start()
方法之后,系统开始调度线程的执行一样。当调度 OK 之后,协程体里的代码会立即执行。
//方便打印出代码执行所在线程
fun log(o: Any?) {
println("[${Thread.currentThread().name}]:$o")
}
GlobalScope.launch {
log(1)
val job = launch() {
log(2)
}
log(3)
}
运行结果可能为:
[DefaultDispatcher-worker-1]:1
[DefaultDispatcher-worker-1]:3
[DefaultDispatcher-worker-1]:2
2.2 LAZY
懒加载模式,launch
方法调用后,并不会立即调度协程的执行。需要手动调用,该协程才会开始调度执行。
GlobalScope.launch {
log(1)
val job = launch(start = CoroutineStart.LAZY) {
log(2)
}
log(3)
}
同样的代码,内部的协程启动模式换成 LAZY
之后,再看执行结果:
[DefaultDispatcher-worker-1]:1
[DefaultDispatcher-worker-1]:3
对比前面的代码,能够很明显地看出 LAZY
与 DEFAULT
的差别。
我们修改代码为:
GlobalScope.launch {
log(1)
val job = launch(start = CoroutineStart.LAZY) {
log(2)
}
job.join() //等待协程的执行结果,这里会触发协程的调度执行
log(3)
}
运行的结果为:
[DefaultDispatcher-worker-1]:1
[DefaultDispatcher-worker-1]:2
[DefaultDispatcher-worker-1]:3
2.3 ATOMIC
这种模式与 DEFAULT
类似,它也是一旦 launch
方法调用后,协程会立即开始调度执行。但很有趣的是,在协程内部没有遇到挂起函数(suspend fun)之前,它不能取消掉。
顺便说一下挂起函数,挂起函数是由 suspend
修饰的函数,它只能在协程内部或挂起函数内调用。可以简单理解为,它能"暂停"该函数的执行,当然这里并不是真的暂停,只是说协程调度器暂时不再调度该协程。
GlobalScope.launch {
log(1)
val job1 = launch(start = CoroutineStart.ATOMIC) {
log(2)
log(22)
}
job1.cancel()
val job2 = launch {
log(3)
log(33)
}
job2.cancel()
val job3 = launch(start = CoroutineStart.ATOMIC) {
log(4)
log(44)
delay(100)
log(444)
}
job3.cancel()
val job4 = launch(start = CoroutineStart.ATOMIC) {
delay(100)
log(5)
}
job4.cancel()
}
这段代码的执行结果为:
[DefaultDispatcher-worker-1]:1
[DefaultDispatcher-worker-3]:2
[DefaultDispatcher-worker-3]:22
[DefaultDispatcher-worker-2]:4
[DefaultDispatcher-worker-2]:44
共创建了4个协程:job1、job2、job3、job4,其中协程job2为默认启动模式,其他的启动模式都为 ATOMIC
,delay(100)
是一个挂起函数调用,相当于 Thread.sleep(100)
的作用,每个协程创建之后立即调用 cancel()
方法取消执行。我们来分析每个结果:
- job1:协程体内没有调用挂起函数,协程体内的代码都被执行了,该协程没有被取消掉;
- job2:协程被取消掉了;
- job3:挂起函数
delay(100)
之前的代码执行了,挂起函数后面的代码没有执行; - job4:协程体内的第一行代码就是挂起函数调用,最终该协程体内的代码都没执行;
从上面的例子中可以看到,DEFAULT
模式启动的协程如果还没调度执行是可以取消掉的,ATOMIC
模式启动的协程如果还没调度执行时就被取消,协程体内第一个挂起函数之前的代码依旧会执行。如果该协程内部没有调用任何挂起函数,则该协程里的代码无论如何也会执行。协程的取消有点像线程的中断一样,suspend 函数又有点像线程里能够抛出中断异常的方法一样。
2.4 UNDISPATCHED
这种模式具备 ATOMIC
的功能,与之不同的是,一旦调用 launch
方法后,该协程会立即在当前线程执行。
GlobalScope.launch {
log(1)
val job1 = launch(start = CoroutineStart.UNDISPATCHED) {
log(2)
delay(100)
log(22)
}
job1.cancel()
val job2 = launch(start = CoroutineStart.UNDISPATCHED) {
log(3)
delay(100)
log(33)
}
log("after job2")
val job3 = launch(start = CoroutineStart.ATOMIC) {
log(4)
delay(100)
log(44)
}
log("after job3")
}
执行结果为:
[DefaultDispatcher-worker-2]:1
[DefaultDispatcher-worker-2]:2
[DefaultDispatcher-worker-2]:3
[DefaultDispatcher-worker-2]:after job2
[DefaultDispatcher-worker-2]:after job3
[DefaultDispatcher-worker-2]:4
[DefaultDispatcher-worker-2]:33
[DefaultDispatcher-worker-2]:44
job1 验证了它不能被取消的功能,job2 中 3
会立即在当前线程执行,所以 3
必然会在 after job2
之前执行,job3 中 4
会等待调度器调度执行,所以他并不会在 after job3
之前执行,4
与 after job3
的执行顺序实质上与协程的调度来决定。
3. CoroutineContext介绍
根据文档里的说明,CoroutineContext 的概念主要有3点:
- It is an indexed set of [Element] instances. 它是一个包含 Element 实例的索引集;
- An indexed set is a mix between a set and a map. 索引集是 set 和 map 的混合结构;
- Every element in this set has a unique [Key]. 这个集合中的每个元素都有一个唯一的 Key;
说的通俗一点,CoroutineContext 就是一个集合 Collection,这个集合既有 set 的特性又有 map 的特性,集合里的元素都是 Element 类型的,每个 Element 类型的元素都有一个类型为 Key 的键。按惯例先来看看类定义:
public interface CoroutineContext {
//操作符'[]'重载,通过 Key 获取 context 中的 Element 类型元素。可直接通过 CoroutineContext[Key] 这种形式来获取与 Key 关联的元素,类似从 List 中取出索引为 index 的某个元素:List[index],从 Map 中取出某个元素则为 Map.get(key)
public operator fun get(key: Key): E?
//聚集函数,函数式编程中出现比较多,想象一下"菲波那切数列求和"就容易理解了
//这里是提供了遍历当前 context 中所有 Element 元素的能力
public fun fold(initial: R, operation: (R, Element) -> R): R
//操作符 '+' 重载,类似 List 中的 List.addAll(list)方法、Map 中的 Map.putAll(map) 方法,将2个集合合并成一个集合
public operator fun plus(context: CoroutineContext): CoroutineContext
//返回一个新的 context,但是该 conext 删除了有指定 Key 的 Element。
public fun minusKey(key: Key<*>): CoroutineContext
//Key的定义,空实现,仅仅只是做一个标识
public interface Key
//Element的定义,同样继承自 CoroutineContext
public interface Element : CoroutineContext {
//每个 Element 都有一个 Key
public val key: Key<*>
public override operator fun get(key: Key): E? =
@Suppress("UNCHECKED_CAST")
if (this.key == key) this as E else null
public override fun 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
}
}
可以发现,CoroutineContext 感觉与 Java 里的 Map 最相似,简直就是一个键为 Key 类型的 Map。众所周知,List、Map 的内部数据结构一般为数组、链表之类的,那么 CoroutineContext 的内部数据结构呢?
查看源码,发现它的底层数据结构是一个叫 CombinedContext
的类来实现的,这是一个内部类,定义如下:
internal class CombinedContext(
private val left: CoroutineContext,
private val element: Element
) : CoroutineContext, Serializable
它有2个参数,left 为 CoroutineContext 类型,element 为就是集合里的元素。看到这个定义是不是特奇怪,既不像数组又不像链表,那么它是怎么具备集合的功能的呢,为此我写了个简单的例子:
class List constructor() {
private var head: E? = null
private var tail: List? = null
constructor(head: E?, tail: List?) : this() {
this.head = head
this.tail = tail
}
fun add(e: E) {
if (head == null) {
head = e
} else {
if (tail == null) {
val nextList = List()
nextList.head = e
tail = nextList
} else {
tail?.add(e)
}
}
}
fun size(): Int = (if (head == null) 0 else 1) + (tail?.size() ?: 0)
}
据说这种叫做 List 的递归定义,有些函数式编程语言中,就是采用这种方式来定义 List 的。它有点像链表,又跟链表不太一样,CombinedContext
与之非常类似,仅仅是头尾位置换了一下,当然它更复杂,我们再来看 plus
方法的具体实现:
public operator fun plus(context: CoroutineContext): CoroutineContext =
if (context === EmptyCoroutineContext) this else
context.fold(this) { acc, element ->
val removed = acc.minusKey(element.key)
if (removed === EmptyCoroutineContext) element else {
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)
}
}
}
整段代码就是一递归的实现,主要逻辑有:
- 除了少数情况外,主要返回的就是 CombinedContext 对象;
- 新返回的 CoroutineContext 对象,包含了 2 个 context 里所包含的全部 Element 元素;
- 在组合形成 CombinedContext 的时候,如果当前 context 里有与要相加的 context 含有相同 Key 的 Element,则当前 context 里的该元素会被删除掉。这就让 CoroutineContext 具备了 Set 的属性,一个 Key,只能取出一个对应的 Element;
- 这里有一个key为 ContinuationInterceptor 的元素,它也是继承自 Element,通常叫做协程上下文拦截器(后面再单独将它)。它有点特殊,不管多少次相加操作之后,它总是出现在最后面。通过一个 context,我们总能最快找到拦截器(避免了递归查找);
下图是主要的继承了 CoroutineContext 的类图:
下面我们来写个例子,验证一下其中的特性:
val scope = MainScope()
val context = scope.coroutineContext
//取出 key 为 ContinuationInterceptor 的元素
println("interceptor: " + context[ContinuationInterceptor])
执行结果为: interceptor: Main
class TestContext : ContinuationInterceptor {
override val key: CoroutineContext.Key<*> = ContinuationInterceptor
override fun interceptContinuation(continuation: Continuation): Continuation {
return continuation
}
}
val scope = MainScope()
//执行 context 的相加操作之后,再取出 key 为 ContinuationInterceptor 的元素
val context = scope.coroutineContext + TestContext()
println("interceptor: " + context[ContinuationInterceptor])
执行结果为:interceptor: com.hjy.kotlinstudy.TestContext@18b8ff07
这里可以看到,context 的相加操作之后,如果加号前后两个 context 都有相同的 key,则最终只保留加号后面的 key 对应的元素。如果这里你看到 context[ContinuationInterceptor]
方法调用,你一定会觉得很奇怪,方括号里的参数应该是一个 Key 类型的对象啊,这里的 ContinuationInterceptor
只是一个继承了 CoroutineContext
的接口啊,其实这只是 Kotlin 的一个特性,在 ContinuationInterceptor 接口里定义了一个如下对象:
companion object Key : CoroutineContext.Key
这个俗称伴生对象,context[ContinuationInterceptor]
等同于 context[ContinuationInterceptor.Key]
,在 kotlin 里直接写类名等同于该类里的伴生对象,以后看到类似的写法也就不会觉得晦涩难懂了。
4. 小结
本文介绍了与协程启动相关的几个主要类,特别是 CoroutineContext,我认为它是协程的核心概念,理解它有助于真正理解协程的内部运行机制。