协程(5) | Job的生命周期

1,483 阅读4分钟

前言

前面文章我们介绍了启动协程的3种方式,以及挂起函数的使用以及原理,并且协程和挂起函数关系非常紧密。这篇文章开始就说一些协程的特性,毕竟上篇文章所说的挂起和恢复,在协程和挂起函数都支持,让不少人有点疑惑。

而协程就是那lambda中的代码块,怎么操作协程或者给它增加特性呢?这里就需要引出Job的概念了,Job是协程的句柄,通过Job我们可以操控协程,所以本篇文章非常关键。

正文

在创建协程的3个方法中,除了runBlocking外,launch的返回值就是Job,而async的返回值是Deferred<T>,这个DeferredJob的子类,所以作为协程句柄的Job就至关重要了,通过Job我们可以实现一些特性。

我们先不急着看Job的实现,我们先来理解一下Job。有一个非常好的比喻,可以把Job和协程的关系,比喻为空调遥控器和空调的关系,都是可以监控、查看状态,以及操控状态。

所以Job可以做下面2件事情:

  1. 使用Job监测协程的生命周期状态

  2. 使用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())
}

由代码可以看出:

  • 通过调用JobisActive属性来判断Job是否处于活跃状态。
  • 通过调用JobisCancelled属性来判断Job是否处于已经取消状态。
  • 通过调用JobisCompleted属性来判断Job是否处于已完成状态。

我们先不急着分析各种状态的转变,我们来看个简单例子:

fun main() = runBlocking {
    val job = launch {
        delay(1000L)
    }
    job.log()
    job.cancel()
    job.log()
    delay(1500L)
}

上面代码运行结果如下:

image.png

可以发现刚开始job是活跃的,但是当调用cancel()方法时,jobisCancelled状态返回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其实是没有启动的,我们来看一下打印:

image.png

第一个红框内就是在调用start()前的状态,是不活跃的,当调用start()后,协程#2开始运行,但是运行时间是1000ms,当协程#1中再调用cancel()后,协程#2就变成了取消状态。

协程生命周期

上面打印中,在最后一个红框中,我们发现在调用完jobcancel方法后,这个job既是已经完成状态,也是已经取消状态,所以我们想搞清楚这个,必须要看一下Job内部的状态以及如何切换。

在官方代码注释中,Job的内部状态如下:

image.png

注意,这些都是内部状态,分析如下:

  • 初始状态:当协程是以懒加载的方式创建的,其初始状态为New,以默认方式创建时,状态为Active
  • 中间状态(Cancelling):当调用cancel()方法或者协程出现fail时,会进入Cancelling取消中的状态,这是一个短暂的状态,一般出现在父协程在等待子线程取消完成的时间段。
  • 中间状态(Completing):当调用complete()方法时会进去Completing完成中的状态,这个也通常出现在父协程等待子协程完成的时间段的短暂状态。
  • 最终状态:即Completed已完成和Cancelled已取消状态。

这里注意一点是:协程认为由于某种原因取消的协程,也是一种完成状态,所以在上面代码中调用了cancel方法后,isCancelledisCompleted这2个属性都是true

由于操控协程状态变化的函数在Job的定义中只有start()cancel(),而异常和正常完成是不需要我们操控的,所以我们来看一下源码中对于暴露对外的3个API解释:

  1. 首先就是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完成的时间段也被看成活跃状态。

  1. 然后是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都是完成状态时,才会变成完成状态。

  1. 最后是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处delay1100ms才能正确打印出job的状态,然后再delay2000ms确保都执行完了,然后打印Process end

但是在实际业务中,我们并不知道job的真实运行时间,假如job的运行时间很长,比如把job中的delay改成10000ms,就会出现Process end已经打印了,但是程序并没有执行完成,因为runBlocking会一直阻塞等待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)
}

上面代码就比较符合我们正常的业务了,当协程完成时会执行回调,我们来看一下打印:

image.png

这里我们就可以正确地监听协程执行完成,并且等待协程执行完成。我们来看一下这里涉及的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方法,结果打印如下:

image.png

可以发现这个效果和上面使用join类似, 都是会挂起后面代码,等待协程执行完再恢复。理解了挂起函数的原理,这个非常容易理解,我们不过多解释了。

下面有个动图,来显示了这几行代码的运行,辅助理解:

await.gif

总结

通过Job我们可以真真切切地获取协程的状态以及控制协程了,也更让协程具体化了,其中的API设计很像是线程的设计,也为我们理解提供了便捷,下面是几个API的小结:

  • cancel()可以取消协程。
  • start()可以配合懒加载启动协程。
  • join()和线程join类似,会等待协程执行完成,这是一个挂起函数。
  • invokeOnCompletion是一个回调,在协程执行完成时回调。
  • Deferrd也是一个Job,其await()函数也是挂起函数,会等待协程执行完成。