美文网首页
Android Kotlin Coroutine(3):Job概

Android Kotlin Coroutine(3):Job概

作者: 云飞扬1 | 来源:发表于2020-02-20 18:01 被阅读0次

    在 Kotlin 中启动一个协程主要有 2 种方式:

    public fun CoroutineScope.launch(
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        block: suspend CoroutineScope.() -> Unit
    ): Job
    
    public fun <T> CoroutineScope.async(
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        block: suspend CoroutineScope.() -> T
    ): Deferred<T>
    

    一种是通过 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<Job> {
            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<Job>
    }
    

    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 最后也被取消掉了。

    4.3 子 Job 如果出现异常会导致父 Job 也被取消掉

    相关文章

      网友评论

          本文标题:Android Kotlin Coroutine(3):Job概

          本文链接:https://www.haomeiwen.com/subject/bzckvctx.html