Android Kotlin Coroutine(3):Job概述

在 Kotlin 中启动一个协程主要有 2 种方式:

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

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T>

一种是通过 launch 启动,一种是通过 async 启动,前者会返回一个 Job 类型的对象,后者会返回一个 Deferred 类型的对象。

1. Job的接口定义

Job 顾名思义就是“工作”的意思,每个协程可以想象成是一个工作任务,启动一个协程就是启动一个工作任务,来看看 Job 接口的主要定义:

//Job 也是继承自 Element,所以它本身也是一个协程上下文 context
public interface Job : CoroutineContext.Element {
    
    //Key对象,如果你看到 context[Job] 的写法, 就知道其实指的是这里的这个伴生对象 Key
    public companion object Key : CoroutineContext.Key<Job> {
        init {
            CoroutineExceptionHandler
        }
    }
    //是否活动状态,必须满足几个条件:该协程已经启动、没有完成、没有被取消
    public val isActive: Boolean
    //是否完成状态
    public val isCompleted: Boolean
    //是否被取消状态
    public val isCancelled: Boolean   
    
    //启动协程,开始调度。如果已经启动了,则返回false。与线程的Thread.start()挺类似
    public fun start(): Boolean
    //挂起当前正在运行的协程,等待该 Job 执行完成。与线程的Thread.join()挺类似
    public suspend fun join()
    //取消该 Job
    public fun cancel(cause: CancellationException? = null)
    //该 Job 的子 Job
    public val children: Sequence<Job>
}

2. Job的几个状态

从前面 Job 的接口定义中可以看到,它与线程 Thread 真的很相似,同样都有好几种不同的运行状态,下面通过几个简单的例子直观的感受一下:

2.1 协程没有启动
val job1 = GlobalScope.launch(start = CoroutineStart.LAZY) {
    println("job1 exec...")
}
println("isActive = ${job1.isActive}, isCompleted = ${job1.isCompleted}, isCancelled = ${job1.isCancelled}")
job1.start()
println("isActive = ${job1.isActive}, isCompleted = ${job1.isCompleted}, isCancelled = ${job1.isCancelled}")

执行结果为:

isActive = false, isCompleted = false, isCancelled = false
isActive = true, isCompleted = false, isCancelled = false

懒加载模式,协程还没启动,所以 isActive = false

2.2 协程正常启动运行
val job1 = GlobalScope.launch {
    println("job1 exec...")
}
println("isActive = ${job1.isActive}, isCompleted = ${job1.isCompleted}, isCancelled = ${job1.isCancelled}")

执行结果为:

isActive = true, isCompleted = false, isCancelled = false
job1 exec...

协程正常启动,所以isActive = true

2.3 协程被取消
val job1 = GlobalScope.launch {
    println("job1 exec...")
}
//直接取消协程运行
job1.cancel()
println("isActive = ${job1.isActive}, isCompleted = ${job1.isCompleted}, isCancelled = ${job1.isCancelled}")

执行结果为:

isActive = false, isCompleted = false, isCancelled = true

协程还没执行完毕,就被取消运行,所以 isCancelled = true

2.4 协程正常完成后取消
val job1 = GlobalScope.launch {
    println("job1 exec...")
}
//暂停一下,让job1能被调度执行完
Thread.sleep(100)
//再次调用取消方法
job1.cancel()
println("isActive = ${job1.isActive}, isCompleted = ${job1.isCompleted}, isCancelled = ${job1.isCancelled}")

执行结果为:

job1 exec...
isActive = false, isCompleted = true, isCancelled = false

协程已经执行完毕了,其 isCompleted 肯定为 true 了,再去调用 cancel() 方法,就没有任何影响了,所以 isCancelled = false。一件已经完成的任务,你再去取消它,是没有任何实际意义的了。

2.5 协程没有启动就取消
val job1 = GlobalScope.launch(start = CoroutineStart.LAZY) {
    println("job1 exec...")
}
job1.cancel()
println("isActive = ${job1.isActive}, isCompleted = ${job1.isCompleted}, isCancelled = ${job1.isCancelled}")

执行结果为:

isActive = false, isCompleted = true, isCancelled = true

协程还没有启动就取消,发现 isCompleted 与 isCancelled 都为 true,与前面几种情况对比一下,才能真正理解这几种状态的变化。

2.6 由于内部异常导致取消

val job1 = GlobalScope.launch(start = CoroutineStart.DEFAULT) {
    println("job1 exec...")
    //模拟一个异常
    val i = 1 / 0
}
//当前线程暂停,让协程先调度执行
Thread.sleep(100)    
println("isActive = ${job1.isActive}, isCompleted = ${job1.isCompleted}, isCancelled = ${job1.isCancelled}")

执行结果为:

job1 exec...
isActive = false, isCompleted = false, isCancelled = true

协程非正常结束运行,相当于系统把它取消掉了,所以其 isCancelled 也为 true 。

2.6 Job 内部状态的流转

在源码里可以看到以下状态图:

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

3. Deferred接口

首先它也是一个 Job,所以它拥有 Job 的一切特性。其次,它能返回一个结果值,这点与 Java 里的 Future 类特别相似(如果你熟悉的话)。它比 Job 多了一个方法:

public suspend fun await(): T

调用该方法时,它会等待该 Job 执行完并返回一个结果值。这是一个 suspend 方法,只能在协程内部调用,它会暂停协程的执行(当然它并不会阻塞线程),当 Job 执行完返回结果后,它又会恢复协程的执行。

一般在这种情况下,你可能会用到它:

GlobalScope.launch { 
    val job1: Deferred = async { 
        //其他异步执行的代码
        1
    }
    val job2: Deferred = async { 
        //其他异步执行的代码
        2
    }   
    //后面的代码,需要等待1个或多个异步任务执行的结果
    val result = job1.await() + job2.await()
}

4. Job的完成及取消机制

从 Job 的接口定义中可以看到,Job 是可以有很多子 Job 的,如果一个 Job 与其他 Job 没有任何关联,那么它的完成及取消就很简单,不会影响到其他 Job 的执行。如果是有父子关系的 Job,那么他们的完成及取消则是会有相互关联关系的。

4.1 Job必须等待它所有的子Job完成它才能完成
val parentJob = GlobalScope.launch {
    println("parent job start")           //①
    //在内部再启动一个协程,会自动形成父子关系
    val childJob1 = launch {
        println("child job1 start")       //②
    }
    println("childJob1: $childJob1")      //③
    val childJob2 = launch {
        println("child job2 start")       //④
        //延迟1秒钟,方便验证结果
        delay(1000)
        println("child job2 after delay") //⑤
    }
    println("childJob2: $childJob2")      //⑥
    println("parent job end")             //⑦
}
//让 childJob1 能正常完成,childJob2 还在执行中
Thread.sleep(500)
parentJob.children?.forEach {            //⑧
    println("child job name: ${it}")
}
println("isActive = ${parentJob.isActive}, isCompleted = ${parentJob.isCompleted}, isCancelled = ${parentJob.isCancelled}")  //⑨
//让 childJob2 也执行完成
Thread.sleep(600)
println("isActive = ${parentJob.isActive}, isCompleted = ${parentJob.isCompleted}, isCancelled = ${parentJob.isCancelled}")  //⑩

执行结果为:

parent job start     //①
childJob1: StandaloneCoroutine{Active}@37438415      //③
child job1 start     //② 
childJob2: StandaloneCoroutine{Active}@173bd32a      //⑥
parent job end       //⑦
child job2 start     //④
child job name: StandaloneCoroutine{Active}@173bd32a     //⑧
isActive = true, isCompleted = false, isCancelled = false      //⑨
child job2 after delay      //⑤
isActive = false, isCompleted = true, isCancelled = false      //⑩
  • 看一下 ⑧ 处的结果,发现此时 parentJob 只有 1 个子 Job childJob2 了。本来 parentJob 应该有2个子 Job 的,但是当运行在此处时,childJob1 已经执行完毕了,childJob2 由于 delay() 函数的缘故,还处于活动状态中,它们内部应该会自动进行关联及清除操作。
  • 看一下 ⑨ 处的结果,在此时 parentJob 里的代码理论上都执行完了,那它不应该是已完成状态吗?其实不然,此时它还有一个子 Job childJob2 处于活动状态没有完成,父 Job 必须等待其所有子 Job 都完成之后,它的状态才能被标记为完成。
  • 执行 ⑩ 的时候,parentJob 以及其2个子 Job 内部的代码都执行完毕,所以这个时候该 Job 的 isCompleted 为 true 了。
4.2 取消父 Job 会同时取消其所有子 Job
val parentJob = GlobalScope.launch {
    println("parent job start")
    val childJob1 = launch {
        println("child job1 start")
    }
    val childJob2 = launch {
        println("child job2 start")
        delay(1000)
        println("child job2 after delay")
    }
    println("parent job end")
}
parentJob.cancel()

执行结果为:

parent job start
parent job end
child job2 start
child job1 start

parentJob 被取消之后,childJob2 最后也被取消掉了。

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

推荐阅读更多精彩内容