Job 其实就是协程的句柄。从某种程度上讲,当我们用 launch 和 async 创建一个协程以后,同时也会创建一个对应的 Job 对象。另外,Job 也是我们理解协程生命周期、结构化并发的关键知识点。通过 Job 暴露的 API,我们还可以让不同的协程之间互相配合,从而实现更加复杂的功能。
一、Job 生命周期
1、job的激活(isActive)、取消(isCancelled)与完成(isCompleted)
在上节课我们学习 launch、async 的时候,我们知道它们两个返回值类型分别是 Job 和 Deferred。源码如下:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job { //job
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> { //Deferred
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyDeferredCoroutine(newContext, block) else
DeferredCoroutine<T>(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
而如果你去看 Deferred 的源代码,你会发现,它其实也是继承自 Job 的。对应的,它只是多了一个泛型参数 T,还多了一个返回类型为T 的 await() 方法。所以,不管是 launch 还是 async,它们本质上都会返回一个 Job 对象。源码如下:
//继承job
public interface Deferred<out T> : kotlinx.coroutines.Job {
public abstract val onAwait: kotlinx.coroutines.selects.SelectClause1<T>
public abstract suspend fun await(): T //返回泛型T
@kotlinx.coroutines.ExperimentalCoroutinesApi public abstract fun getCompleted(): T //返回泛型T
@kotlinx.coroutines.ExperimentalCoroutinesApi public abstract fun getCompletionExceptionOrNull(): kotlin.Throwable?
}
通过 Job 对象,我们主要可以做两件事情:
-
使用 Job 监测协程的生命周期状态;
-
使用 Job 操控协程。
看一个具体的例子:
fun main() {
runBlocking{
val job = launch(start = CoroutineStart.LAZY){ //CoroutineStart.LAZY调用start才能开启协程
delay(1000)
}
job.log(0)
job.start() //开启协程
job.log(1)
delay(500)
job.cancel() //取消协程
job.log(2)
job.invokeOnCompletion { //协程完成或者取消会回调一次
job.log(4)
}
delay(1500)
job.log(3)
}
}
//打印协程的时间
fun Job.log(index:Int) {
print(
"""
---------start $index--------
Time=${LocalDateTime.now()} //时间
ThreadName= ${Thread.currentThread().name} //线程名称
isActive = $isActive //是否激活
isCancelled = $isCancelled //是否被取消
isCompleted = $isCompleted //是否完成
---------end $index--------
"""
)
}
输出的日志:
---------start 0--------
Time=2023-02-22T16:20:43.022617100
ThreadName= main @coroutine#1
isActive = false
isCancelled = false
isCompleted = false
---------end 0--------
---------start 1--------
Time=2023-02-22T16:20:43.023616900
ThreadName= main @coroutine#1
isActive = true
isCancelled = false
isCompleted = false
---------end 1--------
---------start 2--------
Time=2023-02-22T16:20:43.543217500
ThreadName= main @coroutine#1
isActive = false
isCancelled = true
isCompleted = false
---------end 2--------
---------start 4--------
Time=2023-02-22T16:20:43.586217500
ThreadName= main @coroutine#2
isActive = false
isCancelled = true
isCompleted = true
---------end 4--------
---------start 3--------
Time=2023-02-22T16:20:45.053775500
ThreadName= main @coroutine#1
isActive = false
isCancelled = true
isCompleted = true
---------end 3--------
添加CoroutineStart.LAZY参数,Job只有调用start才会开始(激活)协程;invokeOnCompletion会在协程完成或者异常或者取消的情况下完成一次回调。
从协程的三个状态Api可以看出,调用start后,协程isActive = true,调用cancel后isCancelled = true,最后完成回调isCompleted = true。
2、job的挂起(join)
job.join() 其实是一个“挂起函数”,它的作用就是:挂起当前的程序执行流程,等待 job 当中的协程任务执行完毕,然后再恢复当前的程序执行流程。
看下面的例子,如果不加线程的sleep,job协程肯定不会输出job end的,如下:
runBlocking {
println("out start")
val job = GlobalScope.launch {
println("job start")
delay(1000)
println("job end")
}
println("out mid")
job.invokeOnCompletion {
println("job completion")
}
println("out end")
// Thread.sleep(2000) //不加线程的sleep
}
//输出日志
out start
out mid
job start
out end
但如果我们在外部协程执行完线程退出前把job挂起,会怎么样呢?
runBlocking {
println("out start")
val job = GlobalScope.launch {
println("job start")
println("job start thread:${Thread.currentThread().name}")
delay(1000)
println("job end")
println("job end thread:${Thread.currentThread().name}")
}
println("out mid")
println("job join thread:${Thread.currentThread().name}")
job.join() //改变在这里:调用job.join将job协程挂起
job.invokeOnCompletion {
println("job completion")
}
println("out end")
}
//输出日志
out start
out mid
job join thread:main @coroutine#1 //被挂起前为主线程
job start
job start thread:DefaultDispatcher-worker-1 @coroutine#2 //被挂起到子线程执行
job end
job end thread:DefaultDispatcher-worker-1 @coroutine#2 //在子线程执行完成
job completion
out end
被挂起的job协程,会在其他线程执行完后恢复外部协程,这就是协程挂起的魅力。join的源码如下:
public suspend fun join()
也是一个suspend挂起函数。
二、Deferred
Deferred 是继承自 Job 的一个接口,它并没有在 Job 的基础上扩展出很多其他功能,最重要的就是 await() 这个方法。让我们来看一个简单的例子:
runBlocking {
println("out start")
val deferred = async {
println("deferred start")
delay(1000)
"Hello World" //返回Hello World
}
val result = deferred.await() //将deferred挂起,并获取协程的返回值
println("out result:$result")
println("out end")
}
//打印日志
out start
deferred start
out result:Hello World //返回值:Hello World
out end
await() 这个方法是一个挂起函数,这也就意味着,这个方法拥有挂起和恢复的能力。如果当前的 Deferred 任务还没执行完毕,那么,await() 就会挂起当前的协程执行流程,等待 Deferred 任务执行完毕,再恢复执行后面剩下的代码。
看到这里,也许你会觉得奇怪,挂起函数不是非阻塞的吗?怎么这里又出现了阻塞?注意,这里其实只是看起来像是阻塞了,但它实际上是将剩下的代码存了起来,留在后面才执行了。也就是嵌套的协程,如果有协程是阻塞的,那么其他协程都会被阻塞。
Deferred 除了比 Job 多了一个 await() 挂起函数,还多了二个实验性的API,分别是getCompleted,getCompletionExceptionOrNull,这二个API必须在协程完成后才能调用,不然就会抛异常。
三、Job 与结构化并发
结构化并发是 Kotlin 协程的第二大优势。那么,到底什么是结构化并发呢?其 实,这是一个非常大的话题,三言两语真的很难讲清楚。简单来说,结构化并发就是:带有结构和层级的并发。看下面的例子:
runBlocking {
var parentJob: Job? = null
var job1: Job? = null
var job2: Job? = null
var job3: Job? = null
printMsg("parentJob start")
parentJob = launch {
job1 = launch {
printMsg("job1 start")
delay(1000)
printMsg("job1 end")
}
job2 = launch {
printMsg("job2 start")
delay(3000)
printMsg("job2 end")
}
job3 = launch {
printMsg("job3 start")
delay(5000)
printMsg("job3 end")
}
}
delay(500)
//遍历子协程
parentJob.children.forEachIndexed { index, job ->
when (index) {
0 -> println("job1-->job is job1? ${job1 === job}")
1 -> println("job2-->job is job2? ${job2 === job}")
2 -> println("job3-->job is job3? ${job3 === job}")
}
}
//将parentJob协程挂起
parentJob.join()
printMsg("parentJob end")
}
//输出日志
2023-02-23T17:38:15.710147700 main @coroutine#1 parentJob start
2023-02-23T17:38:15.731157500 main @coroutine#3 job1 start
2023-02-23T17:38:15.731157500 main @coroutine#4 job2 start
2023-02-23T17:38:15.731157500 main @coroutine#5 job3 start
job1-->job is job1? true
job2-->job is job2? true
job3-->job is job3? true
2023-02-23T17:38:16.738585700 main @coroutine#3 job1 end
2023-02-23T17:38:18.742618400 main @coroutine#4 job2 end
2023-02-23T17:38:20.732363600 main @coroutine#5 job3 end
2023-02-23T17:38:20.733357900 main @coroutine#1 parentJob end
在上面的代码中,我们一共定义了 4 个 Job,parentJob 是最外层的 launch 返回的对象,而在这个 launch 的内部,还额外嵌套了三个 launch,它们的 Job 对象分别赋值给了 job1、job2、job3。
接着,我们对parentJob.children进行了遍历,然后逐一对比了它们与 job1、job2、job3 的引用是否相等(===代表了引用相等,即是否是同一个对象)。
通过这样的方式,我们可以确定,job1、job2、job3 其实就是 parentJob 的 children。也就是
说,我们使用 launch 创建出来的协程,是存在父子关系的。
如果你去看 Job 的源代码,你会发现它还有两个 API 是用来描述父子关系的。源码如下:
public interface Job : CoroutineContext.Element {
//...省略部分代码...
public val children: Sequence<Job>
@InternalCoroutinesApi
public fun attachChild(child: ChildJob): ChildHandle
}
另外还有ChildJob,ParentJob,ChildHandle接口,源码如下:
public interface ChildJob : Job {
@InternalCoroutinesApi
public fun parentCancelled(parentJob: ParentJob)
}
public interface ParentJob : Job {
@InternalCoroutinesApi
public fun getChildJobCancellationCause(): CancellationException
}
public interface ChildHandle : DisposableHandle {
@InternalCoroutinesApi
public val parent: Job?
@InternalCoroutinesApi
public fun childCancelled(cause: Throwable): Boolean
}
可以看到,每个 Job 对象,都会有一个 children 属性,它的类型是 Sequence,它是一个惰性的集合,我们可以对它进行遍历。而 attachChild() 则是一个协程内部的 API,用于绑定ChildJob 的。
另外注意了,我们调用的是 parentJob 的 join() 方法,它会等待其内部的 job1、job2、job3
全部执行完毕后才会恢复执行。换句话说,只有当 job1、job2、job3 全部执行完毕,parentJob 才算是执行完毕。
如果我们取消parentJob,childJob会怎么样呢?看下面的代码:
runBlocking {
var parentJob: Job? = null
var job1: Job? = null
var job2: Job? = null
var job3: Job? = null
printMsg("parentJob start")
parentJob = launch {
job1 = launch {
printMsg("job1 start")
delay(1000)
printMsg("job1 end")
}
job2 = launch {
printMsg("job2 start")
delay(3000)
printMsg("job2 end")
}
job3 = launch {
printMsg("job3 start")
delay(5000)
printMsg("job3 end")
}
}
//遍历子协程
parentJob.children.forEachIndexed { index, job ->
when (index) {
0 -> println("job1-->job is job1? ${job1 === job}")
1 -> println("job2-->job is job2? ${job2 === job}")
2 -> println("job3-->job is job3? ${job3 === job}")
}
}
delay(500)
//将parentJob协程取消
parentJob.cancel() //变化在这里
printMsg("parentJob end")
}
//输出日志
2023-02-23T18:03:27.189871100 main @coroutine#1 parentJob start
2023-02-23T18:03:27.216872500 main @coroutine#3 job1 start
2023-02-23T18:03:27.216872500 main @coroutine#4 job2 start
2023-02-23T18:03:27.216872500 main @coroutine#5 job3 start
2023-02-23T18:03:27.710896100 main @coroutine#1 parentJob end
即使我们调用的只是 parentJob 的 cancel() 方法,并没有取消 job1、job2、job3,但是它们内部的协程任务也全都被取消了。
四、思考与实战
1、实战示例
在内部定义了三个挂起函数 getResult1()、getResult2()、getResult3(),它们各自都会耗时 1000 毫秒,而且它们之间的运行结果也互不相干。代码逻辑也很简单,也是我们平时在工作中会经常遇到的业务场景。请思考下面的代码应该如何优化?
runBlocking {
//耗时一秒多的挂起函数
suspend fun getResult1(): String {
delay(1000)
return "result1"
}
//耗时一秒多的挂起函数
suspend fun getResult2(): String {
delay(1000)
return "result2"
}
//耗时一秒多的挂起函数
suspend fun getResult3(): String {
delay(1000)
return "result3"
}
val resultList = mutableListOf<String>()
//统计时间
val time = measureTimeMillis {
resultList.add(getResult1())
resultList.add(getResult2())
resultList.add(getResult3())
}
printMsg("time $time")
printMsg("list $resultList")
}
//打印日志
2023-02-23T18:14:48.430632600 main @coroutine#1 time 3041
2023-02-23T18:14:48.431634500 main @coroutine#1 list [result1, result2, result3]
当我们直接调用这三个挂起函数,并且拿到结果以后,整个过程大约需要消耗 3000 毫秒,也就是这几个函数耗时的总和。对于这样的情况,我们其实完全可以使用 async 来优化:
runBlocking {
//耗时一秒多的挂起函数
suspend fun getResult1(): String {
delay(1000)
return "result1"
}
//耗时一秒多的挂起函数
suspend fun getResult2(): String {
delay(1000)
return "result2"
}
//耗时一秒多的挂起函数
suspend fun getResult3(): String {
delay(1000)
return "result3"
}
val resultList = mutableListOf<String>()
//统计时间
val time = measureTimeMillis {
val deferred1 = async { getResult1() }
val deferred2 = async { getResult2() }
val deferred3 = async { getResult3() }
resultList.add(deferred1.await())
resultList.add(deferred2.await())
resultList.add(deferred3.await())
}
printMsg("time $time")
printMsg("list $resultList")
}
//输出日志
2023-02-23T18:21:33.587125300 main @coroutine#1 time 1025
2023-02-23T18:21:33.588134300 main @coroutine#1 list [result1, result2, result3]
时间大概缩短了三分之二,优化的核心逻辑时:不能每一次都调用await挂起函数,耗时主要就是在挂起的过程中,通过并发执行和获取结果。
所以,当我们总是拿 launch 和 async 来做对比的时候,就会不自觉地认为 async 是用来替代launch 的。但实际上,async 最常见的使用场景是:与挂起函数结合,优化并发。
2、示例剖析
如果上面的示例,我们通过async{...}.await()和withContext的子线程来写是否也能优化并发呢? 看下面的代码(部分代码省略):
//async{...}.await()
val resultList = mutableListOf<String>()
//统计时间
val time = measureTimeMillis {
val result1 = async {
printMsg("result1")
getResult1()
}.await() //直接调用await挂起函数
val result2 = async {
printMsg("result2")
getResult2()
}.await() //直接调用await挂起函数
val result3 = async {
printMsg("result3")
getResult3()
}.await() //直接调用await挂起函数
resultList.add(result1)
resultList.add(result2)
resultList.add(result3)
}
//输出日志
2023-02-24T09:27:38.472654700 main @coroutine#2 result1 //同一个线程不同的协程
2023-02-24T09:27:39.491072500 main @coroutine#3 result2 //同一个线程不同的协程
2023-02-24T09:27:40.496829600 main @coroutine#4 result3 //同一个线程不同的协程
2023-02-24T09:27:41.506078100 main @coroutine#1 time 3064 //还是有3秒多
2023-02-24T09:27:41.506078100 main @coroutine#1 list [result1, result2, result3]
//withContext子线程
val resultList = mutableListOf<String>()
//统计时间
val time = measureTimeMillis {
val result1 = withContext(Dispatchers.IO) { //用withContext的子线程,挂起函数
printMsg("result1") //打印线程信息和返回结果
getResult1()
}
val result2 = withContext(Dispatchers.IO) { //用withContext的子线程,挂起函数
printMsg("result2") //打印线程信息和返回结果
getResult2()
}
val result3 = withContext(Dispatchers.IO) { //用withContext的子线程,挂起函数
printMsg("result3") //打印线程信息和返回结果
getResult3()
}
resultList.add(result1)
resultList.add(result2)
resultList.add(result3)
}
//输出日志
2023-02-24T09:24:01.081278100 DefaultDispatcher-worker-1 @coroutine#1 result1 //同一个线程同一个协程
2023-02-24T09:24:02.102147200 DefaultDispatcher-worker-1 @coroutine#1 result2 //同一个线程同一个协程
2023-02-24T09:24:03.111196700 DefaultDispatcher-worker-1 @coroutine#1 result3 //同一个线程同一个协程
2023-02-24T09:24:04.115182600 main @coroutine#1 time 3066 //还是有3秒多
2023-02-24T09:24:04.115182600 main @coroutine#1 list [result1, result2, result3]
因为都是单个协程执行时就挂起并等待输出结果,所以不管是不同线程还是不同的协程,耗时都没有减少,我们看下正确的示例代码输出(部分代码省略):
val resultList = mutableListOf<String>()
//统计时间
val time = measureTimeMillis {
val deferred1 = async {
printMsg("result1")
getResult1()
}
val deferred2 = async {
printMsg("result2")
getResult2()
}
val deferred3 = async {
printMsg("result3")
getResult3()
}
resultList.add(deferred1.await()) //获取结果时才挂起
resultList.add(deferred2.await()) //获取结果时才挂起
resultList.add(deferred3.await()) //获取结果时才挂起
}
//输出日志
2023-02-24T10:50:08.786657300 main @coroutine#2 result1 //同一个线程不同的协程
2023-02-24T10:50:08.790653200 main @coroutine#3 result2 //同一个线程不同的协程
2023-02-24T10:50:08.791644200 main @coroutine#4 result3 //同一个线程不同的协程
2023-02-24T10:50:09.800200700 main @coroutine#1 time 1052 //时间缩短三分之二
2023-02-24T10:50:09.800200700 main @coroutine#1 list [result1, result2, result3]
所以协程的结构化并发一定要注意协程挂起的时机。注意结构化并发的处理。