在 Kotlin 中启动一个协程主要有 2 种方式:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
public fun CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred
一种是通过 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 {
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
}
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 最后也被取消掉了。