Job与协程关系:
Job 其实就是协程的句柄。从某种程度上讲,当我们用 launch 和 async 创建一个协程以后,同时也会创建一个对应的 Job 对象。
Job 生命周期
launch、async 返回值类型分别是 Job 和 Deferred。
// 代码段1
public interface Deferred<out T> : Job {
public suspend fun await(): T
}
以上Deferred 的源代码,它其实也是继承自 Job 的。对应的,它只是多了一个泛型参数 T,还多了一个返回类型为 T 的 await() 方法。所以,不管是 launch 还是 async,它们本质上都会返回一个 Job 对象。
通过 Job 对象,要可以做两件事情:
使用 Job 监测协程的生命周期状态;
使用 Job 操控协程。
举例:
// 代码段2
fun main() = runBlocking {
val job = launch {
delay(1000L)
}
job.log() // ①
job.cancel() // ②
job.log() // ③
delay(1500L)
}
/**
* 打印Job的状态信息
*/
fun Job.log() {
logX("""
isActive = $isActive
isCancelled = $isCancelled
isCompleted = $isCompleted
""".trimIndent())
}
/**
* 控制台输出带协程信息的log
*/
fun logX(any: Any?) {
println("""
================================
$any
Thread:${Thread.currentThread().name}
================================""".trimIndent())
}
/*
输出结果:
================================
isActive = true
isCancelled = false
isCompleted = false
Thread:main @coroutine#1
================================
================================
isActive = false
isCancelled = true
isCompleted = false
Thread:main @coroutine#1
================================
*/
定义一个 Job.log() 扩展函数,它的作用就是打印 Job 的生命周期状态。通过调用这个函数,我们就可以知道对应的协程处于什么状态。
注释①处的调用结果,“isActive = true”,这代表了当前的协程处于活跃状态。注释②,我们调用了 job.cancel() 以后,协程任务就会被取消。因此,注释③处的调用结果就会变成“isCancelled = true”,这代表了协程任务处于取消状态。
job.log(),其实就是在监测协程;job.cancel(),其实就是在操控协程。
除了 job.cancel() 可以操控协程以外,我们还经常使用 job.start() 来启动协程任务,一般搭配“CoroutineStart.LAZY”来使用。
fun main() = runBlocking {
// 变化在这里
// ↓
val job = launch(start = CoroutineStart.LAZY) {
logX("Coroutine start!")
delay(1000L)
}
delay(500L)
job.log()
job.start() // 变化在这里
job.log()
delay(500L)
job.cancel()
delay(500L)
job.log()
delay(2000L)
logX("Process end!")
}
/*
输出结果:
================================
isActive = false
isCancelled = false
isCompleted = false
Thread:main @coroutine#1
================================
================================
isActive = true
isCancelled = false
isCompleted = false
Thread:main @coroutine#1
================================
================================
Coroutine start!
Thread:main @coroutine#2
================================
================================
isActive = false
isCancelled = true
isCompleted = true
Thread:main @coroutine#1
================================
================================
Process end!
Thread:main @coroutine#1
================================
*/
当使用 CoroutineStart.LAZY 作为启动模式的时候,协程任务被 launch 以后,并不会立即执行,即使我们在代码中 delay 了 500 毫秒,launch 内部的"Coroutine start!"也仍然没有输出。这是典型的懒加载行为模式。
在外部调用了 job.start() 以后,job 的状态才变成了 Active 活跃。而当调用了 cancel 以后,job 的状态才变成 isCancelled、isCompleted。
当调用 cancel 以后,isCancelled = true、isCompleted = true。因为Job 内部私有的 Completed、Cancelled 状态,都会认为是外部的 isCompleted 状态。
为了更加灵活地等待和监听协程的结束事件,我们可以用 job.join() 以及 invokeOnCompletion {}
// 代码段6
fun main() = runBlocking {
suspend fun download() {
// 模拟下载任务
val time = (Random.nextDouble() * 1000).toLong()
logX("Delay time: = $time")
delay(time)
}
val job = launch(start = CoroutineStart.LAZY) {
logX("Coroutine start!")
download()
logX("Coroutine end!")
}
delay(500L)
job.log()
job.start()
job.log()
job.invokeOnCompletion {
job.log() // 协程结束以后就会调用这里的代码
}
job.join() // 等待协程执行完毕
logX("Process end!")
}
/*
运行结果:
================================
isActive = false
isCancelled = false
isCompleted = false
Thread:main @coroutine#1
================================
================================
isActive = true
isCancelled = false
isCompleted = false
Thread:main @coroutine#1
================================
================================
Coroutine start!
Thread:main @coroutine#2
================================
================================
Delay time: = 252
Thread:main @coroutine#2
================================
================================
Coroutine end!
Thread:main @coroutine#2
================================
================================
isActive = false
isCancelled = false
isCompleted = true
Thread:main @coroutine#2
================================
================================
Process end!
Thread:main @coroutine#1
================================
*/
invokeOnCompletion {} 的作用,其实就是监听协程结束的事件。需要注意的是,它和前面的 isCompleted 类似,如果 job 被取消了,invokeOnCompletion {} 这个回调仍然会被调用。
job.join() 其实是一个“挂起函数”,它的作用就是:挂起当前的程序执行流程,等待 job 当中的协程任务执行完毕,然后再恢复当前的程序执行流程。
Job 的源代码:
// 代码段7
public interface Job : CoroutineContext.Element {
// 省略部分代码
// ------------ 状态查询API ------------
public val isActive: Boolean
public val isCompleted: Boolean
public val isCancelled: Boolean
public fun getCancellationException(): CancellationException
// ------------ 操控状态API ------------
public fun start(): Boolean
public fun cancel(cause: CancellationException? = null)
public fun cancel(): Unit = cancel(null)
public fun cancel(cause: Throwable? = null): Boolean
// ------------ 等待状态API ------------
public suspend fun join()
public val onJoin: SelectClause0
// ------------ 完成状态回调API ------------
public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
public fun invokeOnCompletion(
onCancelling: Boolean = false,
invokeImmediately: Boolean = true,
handler: CompletionHandler): DisposableHandle
}
Kotlin 官方对 Job 的 API 做了更加详细的划分,但实际上来说都属于“监测状态”“操控状态”这两个大的范畴。
如何理解“Job 是协程的句柄”这句话呢?可以从现实生活中找例子:Job 和协程的关系,就有点像“遥控器和空调的关系”。空调遥控器可以监测空调的运行状态;Job 也可以监测协程的运行状态;空调遥控器可以操控空调的运行状态,Job 也可以简单操控协程的运行状态。所以,从某种程度来讲,遥控器也是空调对外暴露的一个“句柄”。
Deferred
Deferred 其实就是继承自 Job 的一个接口,它并没有在 Job 的基础上扩展出很多其他功能,最重要的就是 await() 这个方法。举例:
// 代码段8
fun main() = runBlocking {
val deferred = async {
logX("Coroutine start!")
delay(1000L)
logX("Coroutine end!")
"Coroutine result!"
}
val result = deferred.await()
println("Result = $result")
logX("Process end!")
}
/*
输出结果:
================================
Coroutine start!
Thread:main @coroutine#2
================================
================================
Coroutine end!
Thread:main @coroutine#2
================================
Result = Coroutine result!
================================
Process end!
Thread:main @coroutine#1
================================
*/
deferred.await() 这个方法,不仅可以获取协程的执行结果,它还会阻塞当前协程的执行流程,直到协程任务执行完毕。在这一点的行为上,await() 和 join() 是类似的。
await() 的函数签名:
// 代码段9
public interface Deferred<out T> : Job {
// 注意这里
// ↓
public suspend fun await(): T
}
await() 这个方法其实是一个挂起函数,这也就意味着,这个方法拥有挂起和恢复的能力。如果当前的 Deferred 任务还没执行完毕,那么,await() 就会挂起当前的协程执行流程,等待 Deferred 任务执行完毕,再恢复执行后面剩下的代码。
一个线程上可以并行执行多个task,await阻塞的是它所在的task的后续程序的执行,而不阻塞在这个线程上的其它task的执行。await() 后面的代码,虽然看起来是阻塞了,但它只是执行流程被挂起和恢复的一种表现。同样。job.join() 的行为模式,在协程执行完毕之前,后面的协程代码都被暂时挂起了,等到协程执行完毕,才有机会继续执行。
Deferred 只是比 Job 多了一个 await() 挂起函数而已,通过这个挂起函数,我们可以等待协程执行完毕的同时,还可以直接拿到协程的执行结果。
Job 与结构化并发
Kotlin 协程的结构化并发,重要性仅次于“挂起函数”。“结构化并发”是 Kotlin 协程的第二大优势。
“结构化并发”就是:带有结构和层级的并发。
举例:
// 代码段10
fun main() = runBlocking {
val parentJob: Job
var job1: Job? = null
var job2: Job? = null
var job3: Job? = null
parentJob = launch {
job1 = launch {
delay(1000L)
}
job2 = launch {
delay(3000L)
}
job3 = launch {
delay(5000L)
}
}
delay(500L)
parentJob.children.forEachIndexed { index, job ->
when (index) {
0 -> println("job1 === job is ${job1 === job}")
1 -> println("job2 === job is ${job2 === job}")
2 -> println("job3 === job is ${job3 === job}")
}
}
parentJob.join() // 这里会挂起大约5秒钟
logX("Process end!")
}
/*
输出结果:
job1 === job is true
job2 === job is true
job3 === job is true
// 等待大约5秒钟
================================
Process end!
Thread:main @coroutine#1
================================
*/
上面定义了 4 个 Job,parentJob 是最外层的 launch 返回的对象,而在这个 launch 的内部,还额外嵌套了三个 launch,它们的 Job 对象分别赋值给了 job1、job2、job3。接着,我们对“parentJob.children”进行了遍历,然后逐一对比了它们与 job1、job2、job3 的引用是否相等(“===”代表了引用相等,即是否是同一个对象)。通过这样的方式,我们可以确定,job1、job2、job3 其实就是 parentJob 的 children。也就是说,我们使用 launch 创建出来的协程,是存在父子关系的。
Job 的源代码,有两个 API 是用来描述父子关系的:
// 代码段11
public interface Job : CoroutineContext.Element {
// 省略部分代码
// ------------ parent-child ------------
public val children: Sequence<Job>
@InternalCoroutinesApi
public fun attachChild(child: ChildJob): ChildHandle
}
每个 Job 对象,都会有一个 children 属性,它的类型是 Sequence,它是一个惰性的集合,我们可以对它进行遍历。而 attachChild() 则是一个协程内部的 API,用于绑定 ChildJob 的。
调用 parentJob 的 join() 方法,它会等待其内部的 job1、job2、job3 全部执行完毕,才会恢复执行。换句话说,只有当 job1、job2、job3 全部执行完毕,parentJob 才算是执行完毕了。
将“parentJob.join”改为“parentJob.cancel()”。即使调用的只是 parentJob 的 cancel() 方法,并没有碰过 job1、job2、job3,但是它们内部的协程任务也全都被取消了。
以结构化的方式构建协程以后,我们的 join()、cancel() 等操作,也会以结构化的模式来执行。
实战举例:
// 代码段13
fun main() = runBlocking {
suspend fun getResult1(): String {
delay(1000L) // 模拟耗时操作
return "Result1"
}
suspend fun getResult2(): String {
delay(1000L) // 模拟耗时操作
return "Result2"
}
suspend fun getResult3(): String {
delay(1000L) // 模拟耗时操作
return "Result3"
}
val results = mutableListOf<String>()
val time = measureTimeMillis {
results.add(getResult1())
results.add(getResult2())
results.add(getResult3())
}
println("Time: $time")
println(results)
}
/*
输出结果:
Time: 3018
[Result1, Result2, Result3]
*/
使用 async 来优化:
// 代码段14
fun main() = runBlocking {
suspend fun getResult1(): String {
delay(1000L) // 模拟耗时操作
return "Result1"
}
suspend fun getResult2(): String {
delay(1000L) // 模拟耗时操作
return "Result2"
}
suspend fun getResult3(): String {
delay(1000L) // 模拟耗时操作
return "Result3"
}
val results: List<String>
val time = measureTimeMillis {
val result1 = async { getResult1() }
val result2 = async { getResult2() }
val result3 = async { getResult3() }
results = listOf(result1.await(), result2.await(), result3.await())
}
println("Time: $time")
println(results)
}
/*
输出结果:
Time: 1032
[Result1, Result2, Result3]
*/
async 最常见的使用场景是:与挂起函数结合,优化并发。
如果任务是 IO 密集型的,代码运行效率是可以实现成倍提升的。万一任务在某些场景下,并发反而会降低效率,也完全可以使用 CoroutineStart 来控制它的启动模式。所以,这种方式的扩展性和灵活性都很好。
小结:
协程是有生命周期的,协程是结构化的。
Job,相当于协程的句柄,Job 与协程的关系,有点像“遥控器与空调的关系”。
Job,在它的内部,维护了一系列的生命周期状态,它也对应着协程的生命周期状态。
通过 Job,我们可以监测协程的状态,比如 isActive、isCancelled、isCompleted;另外,我们也可以一定程度地操控协程的状态,比如 start()、cancel()。
除此之外,我们还可以通过 Job.invokeOnCompletion {} 来监听协程执行完毕的事件;通过 Job.join() 这个挂起函数,我们可以挂起当前协程的执行流程,等到协程执行完毕以后,再恢复执行后面的代码。
而对于 Deferred.await(),它的行为模式和 Job.join() 类似,只是它还会返回协程的执行结果。
另外,协程是结构化的并发,这是它的第二大优势。通过分析 Job 的源码,我们发现,一个 Job 可以拥有多个 ChildJob;对应的,协程也可拥有多个“子协程”。
那么结构化并发带来的最大优势就在于,我们可以实现只控制“父协程”,从而达到控制一堆子协程的目的。在前面的例子中,parentJob.join() 不仅仅只会等待它自身执行完毕,还会等待它内部的 job1、job2、job3 执行完毕。parentJob.cancel() 同理。