协程上下文、协程作用域、Job与Deferred、协程启动模式

1,074 阅读8分钟
  • 本文概览:

    • 什么是协程上下文,有什么用,是如何构成的,如何解构;协程中的作用域的分类,有什么细节;协程的启动模式是什么,有哪几种?协程中的 Job是什么,其与 Deferred接口的区别?

协程上下文:

  • 是什么:Kotlin协程中的一个基本构成单位,包含一些用户定义的数据

  • 有什么用:利用上下文实现协程对线程行为、生命周期、异常、调试等一系列操作

  • CoroutineContext: 协程上下文

    1. 线程行为、生命周期、异常以及调试:

    2. 包含用户定义的一些数据集合,这些数据与协程密切相关

    3. 它是一个有索引的 Element 实例集合,一个介于 set 和 map之间的数据结构。每个 element 在这个集合有一个唯一的 Key

      • 拿到可key就能拿到具体元素
  • 协程上下文的构成:

    • Job(属于上下文的一种): 控制协程的生命周期

    • CoroutineDispatcher: 向合适的线程分发任务

      • 协程在哪里执行
      • 协程看似随机分配在线程上,但实际上还是
      • 类似于RxJava的schedule,控制RxJava到底在那一个线程
    • CoroutineName: 协程的名称,调试的时候很有用

      • 协程的名字是一个哈希码,用这个可以对其重命名
    • CoroutineExceptionHandler: 处理未被捕捉的异常

      • Java中也有全局捕获

协程上下文测试

  • 概述:

    • 可以打印协程上下文各构成部分,并且支持加减操作
  • 代码:PC平台

     fun main(){
         val t = CorountineTest2()
         t.start()
         Thread.sleep(1000)
     }
     class CorountineTest2 {
     ​
         fun coroutineContextTest(){
             val coroutineContext = Job() + Dispatchers.Default + CoroutineName("myContext")
             //get操作符重载去获取协程上下文内的东西
             log("$coroutineContext,${coroutineContext[CoroutineName]}")
             
             //minusKey:可以动态调整协程上下文的打印结果
             val newCoroutineContext = coroutineContext.minusKey(CoroutineName)
             log("$newCoroutineContext")
         }
     }
    
  • 运行结果:

    image-20220609153804419

  • 在android平台(模拟器)上运行:添加代码

     //在按钮的点击事件中添加
     val t = CorountineTest2()
     t.coroutineContextTest()
    
  • 运行结果:模拟器与PC平台一致

    图片.png

源码分析:coroutineContext

  • 概述:

    • coroutineContext为一个接口,内部提供了get()方法
  • 整体结构:

    image-20220609154425004

  • 重要方法:get

    • get:

      • 可以通过 key 来获取这个 Element。由于这是一个 get 操作符,所以可以像访问 map 中的元素一样使用 context[key] 这种中括号的形式来访问
       public operator fun <E: element> get(key:Key<E>): E?
      
    • plus:

      • 和 Set.plus 扩展函数类似,返回一个新的 context 对象,新的对象里面包含了两个里面的所有 Element,如果遇到重复的(Key 一样的),那么用+号右边的 Element 替代左边的。+ 运算符可以很容易的用于结合上下文,但是有一个很重要的事情需要小心 —— 要注意它们结合的次序,因为这个 + 运算符是不对称的。
       public operator fun plus(context: CoroutineContext): CoroutineContext{...}
      
    • fold:

      • 和 Collection.fold 扩展函数类似,提供遍历当前 context 中所有 Element 的能力
       public fun <R> fold(initial: R, operation: (R, Element) -> R): R
      
    • minusKey:

      • 返回一个上下文,其中包含该上下文中的元素,但不包含具有指定key的元素。
        public fun minusKey(key: Key<*>): CoroutineContext
      
  • 重要接口:

    • key:

       public interface Key<E : Element>
      
    • Element:继承自CoroutineContext,通过key找到element

       public interface Element : CoroutineContext {...}
      

Kotlin 中的协程上下文解构效果

  • 加号右侧的元素会覆盖加号左侧的元素,进而组成新创建的 CoroutineContext

  • 代码:

     package com.zero.jiangke
     ​
     import kotlinx.coroutines.*
     ​
     ​
     fun main(){
         val t = CorountineTest2()
         t.coroutineContextTest1()
         Thread.sleep(1000)
     }
     class CorountineTest2 {
         fun coroutineContextTest1(){
             val coroutineContext = Dispatchers.Default + CoroutineName("myContext")
             log("$coroutineContext,${coroutineContext[CoroutineName]}")
             val newCoroutineContext = coroutineContext + Dispatchers.IO //所以加号右侧的元素会覆盖加号左侧的元素,进而组成新创建的 CoroutineContext
             log("$newCoroutineContext")
         }
     }
    
  • 运行结果:

    image-20220609161001093

  • 整体继承关系: 图片.png

Kotlin 协程中的 Job

  • Job 负责管理协程的生命周期:属于element,实际上也是一个上下文

  • 不同 Job的实现具有一定的层级关系

    • 父job可以包含子job

      image-20220609161502209

  • 协程之间是有生命周期转换的

    • 细节:

      • 会等待子 job执行结束,父 job才结束执行
      • 发生异常时进入cancelling状态,接着进入cancelled状态
    • 示意图:从说明文档中来的

      image-20220609161624909

  • Job中的关键方法:

    • start:启动协程

       public fun start(): Boolean
      
      • 执行逻辑:调用该函数来启动这个 Coroutine,如果当前 Coroutine 还没有执行调用该函数返回 true,如果当前 Coroutine 已经执行或者已经执行完毕,则调用该函数返回 false
    • cancel:取消协程

       public fun cancel(): Unit = cancel(null)
      
      • 执行逻辑:通过可选的取消原因取消此作业。 原因可以用于指定错误消息或提供有关取消原因的其他详细信息,以进行调试。
      • 注意cancel函数有多种实现
    • invokeOnCompletion:为 job 设置通知

       public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
      
      • 执行逻辑:通过这个函数可以给 Job 设置一个完成通知,当 Job 执行完成的时候会同步执行这个Handler,通过Handler拿到返回的通知。

      • 执行状态:CompletionHandler 参数代表了 Job 是如何执行完成的。 cause 有下面三种情况:

        • 如果 Job 是正常执行完成的,则 cause 参数为 null
        • 如果 Job 是正常取消的,则 cause 参数为 CancellationException 对象。这种情况不应该当做错误处理,这是任务正常取消的情形。所以一般不需要在错误日志中记录这种情况。
        • 其他情况表示 Job 执行失败了。
      • 这个函数的返回值为 DisposableHandle 对象,如果不再需要监控 Job 的完成情况了, 则可以调用 DisposableHandle.dispose 函数来取消监听。如果 Job 已经执行完了, 则无需调用 dispose 函数了,会自动取消监听

    • join函数:

       public suspend fun join()
      
      • 特别说明:由suspend 修饰,只能在其他suspend函数或者协程作用域中调用执行

      • 执行逻辑:

        • 这个函数会暂停当前所处的 Coroutine直到该Coroutine执行完成。所以 Job 函数一般用来在另外一个 Coroutine 中等待 job 执行完成后继续执行。
        • 当 Job 执行完成后, job.join 函数恢复,这个时候 job 这个任务已经处于完成状态了,而调用 job.join 的Coroutine还继续处于 activie 状态。
        • 请注意,只有在其所有子级都完成后,作业才能完成
      • 该函数的挂起是可以被取消的,并且始终检查调用的Coroutine的Job是否取消。如果在调用此挂起函数或将其挂起时,调用Coroutine的Job被取消或完成,则此函数将引发 CancellationException

Kotlin协程中的Deferred接口

  • 整体示意:

    image-20220609163222653

  • 重要方法:await()

     public suspend fun await(): T  
    
    • 执行逻辑:类似于Furture

      • 用来等待这个Coroutine执行完毕并返回结果

Kotlin 中的suspend关键字

  • 工作机制:被修饰的函数若为耗时操作,遇到挂起点时将其修饰的函数则会压入一个栈,主线程继续执行;并不会阻塞当前线程;当耗时操作返回完,再恢复到挂起点进行执行;

Kotlin 中的CoroutineDispatcher

  • 总结:

    • 由于子Coroutine 会继承父Coroutine 的 context,所以为了方便使用,我们一般会在 父Coroutine 上设定一个 Dispatcher,然后所有 子Coroutine 自动使用这个 Dispatcher
  • 四种方式:

    • Dispatchers.Default:内部是Java线程池

      • 默认的调度器,适合处理后台计算,是一个CPU密集型任务调度器。如果创建 Coroutine 的时候没有指定 dispatcher,则一般默认使用这个作为默认值。Default dispatcher 使用一个共享的后台线程池来运行里面的任务。注意它和IO共享线程池,只不过限制了最大并发数不同。
    • Dispatchers.IO:RxJava中也有一个这样的I/O

      • 顾名思义这是用来执行阻塞 IO 操作的,是和Default共用一个共享的线程池来执行里面的任务。根据同时运行的任务数量,在需要的时候会创建额外的线程,当任务执行完毕后会释放不需要的线程。
    • Dispatchers.Unconfined:

      • 由于Dispatchers.Unconfined未定义线程池,所以执行的时候默认在启动线程。遇到第一个挂起点,之后由调用resume的线程决定恢复协程的线程。
    • Dispatchers.Main:

      • 指定执行的线程是主线程,在Android上就是UI线程·

Kotlin 中的协程的启动模式:CoroutineStart

  • 四种启动模式:

    • CoroutineStart.DEFAULT:

      • 协程创建后立即开始调度,在调度前如果协程被取消,其将直接进入取消响应的状态 虽然是立即调度,但也有可能在执行前被取消
    • CoroutineStart.ATOMIC:

      • 协程创建后立即开始调度,协程执行到第一个挂起点之前不响应取消;虽然是立即调度,但其将调度和执行两个步骤合二为一了,就像它的名字一样,其保证调度和执行是原子操作,因此协程也一定会执行
    • CoroutineStart.LAZY:

      • 只要协程被需要时,包括主动调用该协程的start、join或者await等函数时才会开始调度,如果调度前就被取消,协程将直接进入异常结束状态
    • CoroutineStart.UNDISPATCHED:

      • 协程创建后立即在当前函数调用栈中执行,直到遇到第一个真正挂起的点;是立即执行,因此协程一定会执行
  • 使用细节:

    • 对于launch:第一个参数就是设置协程的启动模式的

    • 代码:

       //点击事件添加代码:
       val t = CoroutineTest2()
       t.start()
       ​
       fun start(){
           log("start ....")
           val launchJob = GlobalScope.launch(context = Dispatchers.Main,start = CoroutineStart.LAZY) {
               Thread.sleep(10000)//可能导致ANR
               log("launch 启动一个协程")
           }
           Thread.sleep(2000)
           launchJob.start()
           log("launchJob= $launchJob")
       }
      
    • 运行截图:launch默认是会新开一个线程执行协程,但可以通过指定协程上下文,指定协程附着在UI线程上(这个时候,以launch方式启动的协程就会阻塞主线程了)

      图片.png

    • 协程只是说:异步代码同步化,将任务进一步分割了,至于说会不会阻塞UI线程,要看你是怎么玩的

Kotlin 中协程作用域

  • 包含协程上下文:控制协程生命周期、启动模式、异常处理;

  • 每一个协程创建是在协程作用域上的:CoroutineScope 只是定义了一个新 Coroutine 的执行 Scope。每个 coroutine builder 都是 CoroutineScope 的扩展函数,并且自动的继承了当前 Scope 的 coroutineContext分类及行为规则

  • 协程作用域分类:

    • 顶级作用域:

      • 没有父协程的协程所在的作用域为顶级作用域
    • 协同作用域:

      • 协程中启动新的协程,新协程为所在协程的子协程,这种情况下,子协程所在的作用域默认为协同作用域。此时子协程抛出的未捕获异常,都将传递给父协程处理,父协程同时也会被取消。

      • 代码:

         coroutineScope 内部的异常会向上传播,子协程未捕获的异常会向上传递给父协程,任何一个子协程异常退出,会导致整体的退出
             public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
             contract {
                 callsInPlace(block, InvocationKind.EXACTLY_ONCE)
             }
             return suspendCoroutineUninterceptedOrReturn { uCont ->
                 val coroutine = ScopeCoroutine(uCont.context, uCont)
                 coroutine.startUndispatchedOrReturn(coroutine, block)
             }
         }
        
    • 主从作用域(强调父子关系):发生异常,父协程不会受影响,兄弟也不会受影响

      • 与协同作用域在协程的父子关系上一致,区别在于,处于该作用域下的协程出现未捕获的异常时,不会将异常向上传递给父协程。

      • 代码:

         supervisorScope属于主从作用域,会继承父协程的上下文,它的特点就是子协程的异常不会影响父协程
             public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
                 contract {
                     callsInPlace(block, InvocationKind.EXACTLY_ONCE)
                 }
                 return suspendCoroutineUninterceptedOrReturn { uCont ->
                     val coroutine = SupervisorCoroutine(uCont.context, uCont)
                     coroutine.startUndispatchedOrReturn(coroutine, block)
                 }
             }