Android Kotlin Coroutine(2):协程的启动以及协程上下文

前面入门时讲过一个最简单的例子,通过 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

对比前面的代码,能够很明显地看出 LAZYDEFAULT 的差别。

我们修改代码为:

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为默认启动模式,其他的启动模式都为 ATOMICdelay(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 之前执行,4after job3 的执行顺序实质上与协程的调度来决定。

3. CoroutineContext介绍

根据文档里的说明,CoroutineContext 的概念主要有3点:

  1. It is an indexed set of [Element] instances. 它是一个包含 Element 实例的索引集;
  2. An indexed set is a mix between a set and a map. 索引集是 set 和 map 的混合结构;
  3. 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 <E : Element> get(key: Key<E>): E?

    //聚集函数,函数式编程中出现比较多,想象一下"菲波那切数列求和"就容易理解了
    //这里是提供了遍历当前 context 中所有 Element 元素的能力
    public fun <R> 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<E : Element>
    
    //Element的定义,同样继承自 CoroutineContext
    public interface Element : CoroutineContext {
    
        //每个 Element 都有一个 Key
        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
    }    
}

可以发现,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<E> constructor() {
    private var head: E? = null
    private var tail: List<E>? = null

    constructor(head: E?, tail: List<E>?) : this() {
        this.head = head
        this.tail = tail
    }

    fun add(e: E) {
        if (head == null) {
            head = e
        } else {
            if (tail == null) {
                val nextList = List<E>()
                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)
                    }
                }
            }

整段代码就是一递归的实现,主要逻辑有:

  1. 除了少数情况外,主要返回的就是 CombinedContext 对象;
  2. 新返回的 CoroutineContext 对象,包含了 2 个 context 里所包含的全部 Element 元素;
  3. 在组合形成 CombinedContext 的时候,如果当前 context 里有与要相加的 context 含有相同 Key 的 Element,则当前 context 里的该元素会被删除掉。这就让 CoroutineContext 具备了 Set 的属性,一个 Key,只能取出一个对应的 Element;
  4. 这里有一个key为 ContinuationInterceptor 的元素,它也是继承自 Element,通常叫做协程上下文拦截器(后面再单独将它)。它有点特殊,不管多少次相加操作之后,它总是出现在最后面。通过一个 context,我们总能最快找到拦截器(避免了递归查找);

下图是主要的继承了 CoroutineContext 的类图:

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 <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> {
        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<ContinuationInterceptor>

这个俗称伴生对象,context[ContinuationInterceptor] 等同于 context[ContinuationInterceptor.Key],在 kotlin 里直接写类名等同于该类里的伴生对象,以后看到类似的写法也就不会觉得晦涩难懂了。

4. 小结

本文介绍了与协程启动相关的几个主要类,特别是 CoroutineContext,我认为它是协程的核心概念,理解它有助于真正理解协程的内部运行机制。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,082评论 5 464
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,231评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,047评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,977评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,893评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,976评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,605评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,888评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,906评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,732评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,513评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,980评论 3 301
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,132评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,447评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,027评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,232评论 2 339

推荐阅读更多精彩内容