前言
前面文章我们介绍了启动协程的3种方式,以及挂起函数的使用以及原理,并且协程和挂起函数关系非常紧密。这篇文章开始就说一些协程的特性,毕竟上篇文章所说的挂起和恢复,在协程和挂起函数都支持,让不少人有点疑惑。
而协程就是那lambda
中的代码块,怎么操作协程或者给它增加特性呢?这里就需要引出Job
的概念了,Job
是协程的句柄,通过Job
我们可以操控协程,所以本篇文章非常关键。
正文
在创建协程的3个方法中,除了runBlocking
外,launch
的返回值就是Job
,而async
的返回值是Deferred<T>
,这个Deferred
是Job
的子类,所以作为协程句柄的Job
就至关重要了,通过Job
我们可以实现一些特性。
我们先不急着看Job
的实现,我们先来理解一下Job
。有一个非常好的比喻,可以把Job
和协程的关系,比喻为空调遥控器和空调的关系,都是可以监控、查看状态,以及操控状态。
所以Job
可以做下面2件事情:
-
使用
Job
监测协程的生命周期状态; -
使用
Job
操控协程。
打印协程状态
对于空调来说,有停在、运行中等状态,那协程有哪些状态呢?深刻理解协程的状态,以及协程为什么有这几种状态至关重要,因为这涉及协程的结构化并发的特性。
协程的外部管程状态主要分为:是否活跃、是否已经取消和是否已经完成3种状态,我们通过下面打印来获取:
fun Job.log() {
logX("""
isActive = $isActive
isCancelled = $isCancelled
isCompleted = $isCompleted
""".trimIndent())
}
/**
* 控制台输出带协程信息的log
*/
fun logX(any: Any?) {
println("""
================================
$any
Thread:${Thread.currentThread().name}
================================""".trimIndent())
}
由代码可以看出:
- 通过调用
Job
的isActive
属性来判断Job
是否处于活跃状态。 - 通过调用
Job
的isCancelled
属性来判断Job
是否处于已经取消状态。 - 通过调用
Job
的isCompleted
属性来判断Job
是否处于已完成状态。
我们先不急着分析各种状态的转变,我们来看个简单例子:
fun main() = runBlocking {
val job = launch {
delay(1000L)
}
job.log()
job.cancel()
job.log()
delay(1500L)
}
上面代码运行结果如下:
可以发现刚开始job
是活跃的,但是当调用cancel()
方法时,job
的isCancelled
状态返回true
,表示该job
已经被取消了,这也是符合我们预期效果的。
监测和操控协程
上面代码中的job.log()
其实就是监测协程状态,而job.cancel()
就是在操控协程,而这个操控的动作就是取消协程。
看到这里,我们可以类比Java的线程,我们也可以监控线程的状态,和操作线程。
而除了job.cancel()
可以操控协程外,还可以使用job.start()
来启动协程任务。
在前面我们所说的创建协程方法,都是在创建完协程后立即执行了,想使用start()
方法就需要和懒加载配合使用:
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!")
}
上面代码我们使用懒加载模式来启动协程,虽然已经delay
500ms了,我们可以因为没有调用start()
,这个job
其实是没有启动的,我们来看一下打印:
第一个红框内就是在调用start()
前的状态,是不活跃的,当调用start()
后,协程#2开始运行,但是运行时间是1000ms,当协程#1中再调用cancel()
后,协程#2就变成了取消状态。
协程生命周期
上面打印中,在最后一个红框中,我们发现在调用完job
的cancel
方法后,这个job
既是已经完成状态,也是已经取消状态,所以我们想搞清楚这个,必须要看一下Job
内部的状态以及如何切换。
在官方代码注释中,Job
的内部状态如下:
注意,这些都是内部状态,分析如下:
- 初始状态:当协程是以懒加载的方式创建的,其初始状态为
New
,以默认方式创建时,状态为Active
。 - 中间状态(
Cancelling
):当调用cancel()
方法或者协程出现fail
时,会进入Cancelling
取消中的状态,这是一个短暂的状态,一般出现在父协程在等待子线程取消完成的时间段。 - 中间状态(
Completing
):当调用complete()
方法时会进去Completing
完成中的状态,这个也通常出现在父协程等待子协程完成的时间段的短暂状态。 - 最终状态:即
Completed
已完成和Cancelled
已取消状态。
这里注意一点是:协程认为由于某种原因取消的协程,也是一种完成状态,所以在上面代码中调用了cancel
方法后,isCancelled
和isCompleted
这2个属性都是true
。
由于操控协程状态变化的函数在Job
的定义中只有start()
和cancel()
,而异常和正常完成是不需要我们操控的,所以我们来看一下源码中对于暴露对外的3个API解释:
- 首先就是
isActive
的官方解释:
/**
* Returns `true` when this job is active -- it was already started and has not completed nor was cancelled yet.
* The job that is waiting for its [children] to complete is still considered to be active if it
* was not cancelled nor failed.
*/
public val isActive: Boolean
分析可知:当job
处于活跃状态下返回true
,它表示已经开始了,但是还没有完成和被取消的时间段状态。在父job
没有被取消和出现错误的情况下,在等待子job
完成的时间段也被看成活跃状态。
- 然后是
isCompleted
的官方解释:
/**
* Returns `true` when this job has completed for any reason. A job that was cancelled or failed
* and has finished its execution is also considered complete. Job becomes complete only after
* all its [children] complete.
*/
public val isCompleted: Boolean
分析可知:当job
在任何原因下完成都是会返回true
,包括被取消、发生错误和执行完成。同理,父job
也会等到所有的子job
都是完成状态时,才会变成完成状态。
- 最后是
isCancelled
的官方解释:
/**
* Returns `true` if this job was cancelled for any reason, either by explicit invocation of [cancel] or
* because it had failed or its child or parent was cancelled.
* In the general case, it does not imply that the
* job has already [completed][isCompleted], because it may still be finishing whatever it was doing and
* waiting for its [children] to complete.
*/
public val isCancelled: Boolean
分析可知:当job
在任何原因下被取消会返回true
,这些原因包括:显示调用cancel
方法,出现失败,或者它的父job
或者子job
被取消了。在通常情况下,这种状态并不意味着job
已经完成,因为它还有可能还在等待子job
完成。
join()和invokeOnCompletion
说完协程的状态以及改变状态的方法,我们继续来看join()
方法,因为这个方法在很多通用概念中都有,比如在线程知识体系就有这个方法。
我们先来看个例子:
fun main() = runBlocking {
val job = launch(start = CoroutineStart.LAZY) {
logX("Coroutine start!")
delay(1000L)
}
delay(500L)
job.log()
job.start()
job.log()
delay(1100L) // 1
job.log()
delay(2000L) // 2
logX("Process end!")
}
这里我们为了能正常让job
这个协程执行完毕,我们必须在注释1处delay
1100ms才能正确打印出job
的状态,然后再delay
2000ms确保都执行完了,然后打印Process end
。
但是在实际业务中,我们并不知道job
的真实运行时间,假如job
的运行时间很长,比如把job
中的delay
改成10000ms,就会出现Process end
已经打印了,但是程序并没有执行完成,因为runBlockin
g会一直阻塞等待job
执行完毕。
这种靠猜的方式肯定不可取,那如果能等待和监听协程的结束事件就好了,这里就可以使用job.join()
和invokeOnCompletion{}
来优化,下面代码:
fun main() = runBlocking {
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!")
}
suspend fun download() {
// 模拟下载任务
val time = (Random.nextDouble() * 1000).toLong()
logX("Delay time: = $time")
delay(time)
}
上面代码就比较符合我们正常的业务了,当协程完成时会执行回调,我们来看一下打印:
这里我们就可以正确地监听协程执行完成,并且等待协程执行完成。我们来看一下这里涉及的2个方法:
-
invokeOnCompletion
:这个是Job
接口中的一个方法,一旦job
完成就会被回调。 -
join
:join
的翻译就是"加入"的意思,在Java的线程中:在当前主线程中,如果有子线程加入,主线程会进行等待,直到子线程执行完成。在
Job
这里,join
是一个挂起函数:public suspend fun join()
,它的含义是:挂起当前协程,直到job
执行完成。就比如上面的代码中,我们使用
job.join()
,它就会挂起当前协程执行流程,等待job
执行完,才继续执行后续代码。
Deferred
说完可以挂起的join()
方法,是不是会回忆起之前文章所说的await
方法,它也是挂起协程,等待协程执行完,拿到结果。
所以我们来看一下async
启动协程的返回值类型Deferred
的原理,首先它还是一个接口,而是是继承至Job
:public interface Deferred<out T>: Job
,在里面定义了挂起函数await()
:public suspend fun await():T
,我们来看一下测试代码:
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!")
}
这里在外面协程没有调用任何delay
或者join
方法,结果打印如下:
可以发现这个效果和上面使用join
类似, 都是会挂起后面代码,等待协程执行完再恢复。理解了挂起函数的原理,这个非常容易理解,我们不过多解释了。
下面有个动图,来显示了这几行代码的运行,辅助理解:
总结
通过Job
我们可以真真切切地获取协程的状态以及控制协程了,也更让协程具体化了,其中的API设计很像是线程的设计,也为我们理解提供了便捷,下面是几个API的小结:
cancel()
可以取消协程。start()
可以配合懒加载启动协程。join()
和线程join
类似,会等待协程执行完成,这是一个挂起函数。invokeOnCompletion
是一个回调,在协程执行完成时回调。Deferrd
也是一个Job
,其await()
函数也是挂起函数,会等待协程执行完成。