当前位置: 首页>后端>正文

kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时)

  1. 什么是协程?
    • 项目中的需求特别是复杂的需求并不是单一任务的,可能是一个接一个任务组成的,特别是下一个任务需要上一个任务执行完成提供数据,此种情形被称为异步编程(需要注意的是此异步和我们平时说的通过线程实现异步不一样),此异步是代码阶段上说的异步编程,平时的代码通常都是顺序去写,顺序执行,但是此时由于任务场景(一个任务中小任务的串联)需要异步去写代码,java中通常使用Listener,Future,Rx去写,kotlin提供了协程的模式去写异步编程(简单说就是用顺序写代码实现异步的场景)
    • 协程与并发:线程是通过系统调度多线程同时执行逻辑实现并发提升响应速度等,协程则是将单一任务拆分多个小的任务,每个小任务声明为一个协程,通过协程的同时并发提升整体任务的执行时间,进而提升响应速度。


      kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时),第1张
      协程并发
    • 协程和线程的区别:协程通常被称为轻量级的线程。
      • 协程支持线程调度,但是可以多个协程在同一个线程中执行,即一个线程可以创建多个协程,且协程的挂起通常不会阻塞当前线程。
      • 线程的创建,销毁和调度都是由系统去处理的,而协程则不是,协程是由开发者控制的。
      • 由上可知,线程是很消耗资源的,协程则不然,也就是说单一线程中创建上万个协程都不会出现上万个线程造成的oom等。
  2. 协程的语法
    • 协程域构建器 { 协程构建器 { 协程函数体( 挂起函数 )} 协程函数体},即:


      kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时),第2张
      简单协程函数
  3. 协程的基本概念:
    • 协程域构建器:
      • 协程域:协程的作用范围,即协程的生命周期,和其他一致,即声明的线程只在这个作用范围内使用。即:


        kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时),第3张
        协程作用域
    • 协程构建器:即创建协程的语法,包括下面几种:
      • CoroutineScope.launch {}:创建新的协程的最通用的方式之一,创建的协程不会阻塞当前线程,且在参数中可以指定调度器(线程)。返回的是job类型。
      • CoroutineScope.async {}:可以实现与 launch builder 一样的效果,在后台创建一个新协程,唯一的区别是它有返回值,因为CoroutineScope.async {}返回的是 Deferred 类型。
      • runBlocking {}:创建一个新的协程,但是当前协程会阻塞线程,一般不用在逻辑中通常是作为测试使用。
      • withContext {}不会创建新的协程,在指定协程上运行挂起代码块,并挂起该协程直至代码块运行完成,还有就是协程中可以使用其进行协程的环境的切换。
    • 协程调度器(协程派发器):协程调度器可以理解为rxjava中的线程调度器,即指定当前协程在那一个线程中执行。
public actual object Dispatchers {
public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
public val IO: CoroutineDispatcher = DefaultScheduler.IO
}
  • Default:默认调度器,CPU密集型任务调度器,通常处理一些单纯的计算任务,或者执行时间较短任务。例如数据计算 至少两个线程,最多cpu个数线程。如果作用域scope中没有指定线程的时候默认线程是Default,即新建一个线程去执行。
  • IO:IO调度器,IO密集型任务调度器,适合执行IO相关操作。比如:网络请求,数据库操作,文件操作等
  • Main:UI调度器,只有在UI编程平台上有意义,用于更新UI,例如Android中的主线程
  • Unconfined:非受限调度器,无所谓调度器,当前协程可以运行在任意线程上
    • Unconfined:协程派发器会在调用者线程内启动协程, 但只会持续运行到第一次挂起点为止. 在挂起之后, 它会在哪个线程内恢复协程的执行, 这完全由被调用的挂起函数来决定
    • 非受限派发器(Unconfined dispatcher) 适用的场景是, 协程不占用 CPU 时间, 也不更新那些限定于某个特定线程的共享数据(比如 UI)
    • 简单说就是此调度机制是协程不需要切换线程,原来在那个线程就在那个线程,但是其子线程的执行线程取决于这个子协程上一个协程的执行线程,即第一个子协程依赖于启动协程的scope,下一个协程取决于上一个协程的执行线程。


      kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时),第4张
      非受限协程调度器
  • newSingleThreadContext: 将会在独自的新线程中执行,一个专用的线程是一种非常昂贵的资源. 在真实的应用程序中, 这样的线程, 必须在不再需要的时候使用 close函数释放它, 或者保存在一个顶层变量中, 并在应用程序内继续重用
  • 如上所述:launch和async不传递参数(即不指定对应的执行线程)的时候,其执行线程将会继承与新建协程的作用域所在的线程。
  • withContext(ctx2):次函数上面也说了不会创建协程,但是其能够在一个协程中切换协程的执行线程。
  • coroutineContext[Job]:通过此函数能够获取到此协程的对象信息及其是否激活的信息,在调试协程的时候可以用起打印log来方便调试协程。
    • CoroutineContext,协程上下文,是一些元素的集合,主要包括 Job 和 CoroutineDispatcher 元素,可以代表一个协程的场景。可以类同于Android的Context(制订了协程的执行环境)
    • Job,任务,封装了协程中需要执行的代码逻辑。Job 可以取消并且有简单生命周期,它有三种状态:isActive,isCompleted,isCancelled。Job 完成时是没有返回值的,如果需要返回值的话,应该使用 Deferred,它是 Job 的子类。


      kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时),第5张
      协程的返回对象
  1. 挂起函数:
    • 使用修饰符suspend标记的函数被称为挂起函数。
      • 挂起函数和普通函数的参数及其返回值语法一致
      • 挂起函数调用的时候会挂起协程且不会阻塞当前协程所在的线程,待此挂起函数执行完毕会自动重启协程进行后续的执行。
      • 挂起函数在协程中或者在另外一个挂起函数中,且协程中通常至少一个挂起函数。
      • 挂起修饰符suspend修饰符可以标记普通函数、扩展函数和lambda表达式。


        kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时),第6张
        挂起函数
    • 针对协程中的多挂起函数,其执行顺序即挂起函数的代码顺序。
    • 使用async在一个协程中修饰多个挂起函数则多个挂起函数可以并发执行:
      • 概念上来说, async 就好象 launch 一样. 它启动一个独立的协程, 也就是一个轻量的线程, 与其他所有协程一起并发执行。
      • 和launch不同的是 async返回的不是一个job 而是Deferred(轻量级的非阻塞的future)通过其可以获取到协程的最终返回值,且deferred是job的子类,同样可以取消。
      • async可以实现协程的并发执行,若多协程并发执行则必须显示声明。


        kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时),第7张
        多协程并发
    • 使用async (Lazily started) 实现协程的延迟执行,即协程在其父协程调用的时候就开始执行,若不想其此时执行而是程序员控制其执行的时刻,则可以通过此语法设置。此语法设置以后仅在调用job的start和await访问协程的结果的时候才会真正的去执行。


      kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时),第8张
      延迟执行挂起函数
      • GlobalScope.async:不推荐在此作用域中使用async创建协程,因为在此作用域中当前协程出现异常并不能够协程取消而是继续执行。
      • 针对上面的逻辑使用 async 的结构化并发替代解决,即:


        kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时),第9张
        async并发

        * 通过这种方式, 如果 concurrentSum 函数内的某个地方发生错误, 抛出一个异常, 那么在这个函数的作用范围内启动的所有协程都会被取消.
        * Global和后面的区别是定义的作用域不同,后面的作用域不是全局,则后面的协程异常能够导致协程取消。

  2. 协程的取消:协程会返回一个job,通过job可以获取到协程的状态及其通过job的cancel函数可以取消协程的执行,job的join函数等待job的取消完成。
    • job提供一个函数(cancelAndJoin)是上面两个函数的结合体,也就是取消一个协程的执行可以调用上面两个函数,也可以调用这一个函数。
    • 协程的取消和java中的线程取消是一致的,java的线程是不会停止执行的,若想停止线程的执行需要用户自定义一个状态值,在线程执行任务的几个关键节点上校验上面状态值,需要取消的时候不再执行线程中任务的后续,协程的取消也是这样,不过协程的取消在停止线程的执行的同时会抛出异常:CancellationException。
      • kotlin协程库中提供的挂起函数都是处理了取消逻辑的也就是说协程库中的所有挂起函数都可以被取消。
      • 综合上面两条:如果在协程中没有通过协程的状态处理协程的取消,则当前协程调用取消逻辑后并不会停止当前协程的执行(特别是计算密集型任务协程:计算过程中会产生大量的中间数据,如果直接能取消,这些数据变得无效,最终数据也会变得不准确,针对这种任务也是一种保护的),如下:


        kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时),第10张
        协程中计算逻辑不会停止

        kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时),第11张
        • 针对上面提到的计算型任务协程的取消的方法如下:
          • 在计算逻辑的核心节点中使用isactive校验当前的协程是否被取消,若没有取消继续执行若取消则不再执行。
          • 使用函数ensureActive 在计算逻辑的核心节点上代替isactive进行自动校验当前协程是否被取消。
          • 使用kotlin的协程提供的函数yield函数(提供的一个挂起当前协程,执行兄弟协程的函数),此函数在挂起当前协程的基础上会校验当前协程是否取消。
        • 使用 finally 语句来关闭资源:
          • 可被取消的挂起函数, 在被取消时会抛出 CancellationException 异常, 这个异常可以通过通常的方式来处理. 比如, 可以使用 try {...} finally {...} 表达式, 或者 Kotlin 的 use 函数, 以便在一个协程被取消时执行结束处理:
            kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时),第12张
            finally的用法
            • 在finally中通常是处理善后逻辑,关闭资源等,通常这些函数都不是挂起函数,且通常都是非阻塞的,如果在finally中使用挂起函数则使用语法:使用 withContext 函数和 NonCancellable 上下文, 把相应的代码包装在 withContext(NonCancellable) {...} 内, 如下例所示:
              kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时),第13张
              finally中挂起函数语法
          • 针对根协程和协程内多协程的取消:根协程的取消会将根协程取消且传递到他所有的子协程,但是某一个子协程的取消仅仅影响这个协程,并不会影响其他协程和根协程。
  3. 协程的超时:
    • kotlin协程提供了withTimeout(1300L) 语法开启一个1.3s的协程,该协程执行时间最多1.3s 到1.3s无论协程逻辑执行是否完成都会结束并抛出异常TimeoutCancellationException。
    • 超时处理和取消一致,针对其资源也可以通过finally去处理,处理的方式也一样。


      kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时),第14张
      协程的超时处理
    • 也可以使用 withTimeoutOrNull 函数, 它与 withTimeout 函数类似, 但在超时发生时, 它会返回 null, 而不是抛出异常:
      kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时),第15张
      withTimeoutOrNull 函数用法
  4. 协程的异常处理:
    • 未捕获异常:协程构建器launch构建协程,协程中出现异常被当做未捕获异常。针对未捕获异常有两种处理方式:
      • 未捕获异常的通常处理是输出到控制台。
      • 通过根协程的上下文元素 CoroutineExceptionHandler 对根协程及其所有的子协程的异常进行捕获,捕获后统一处理(通常是打印输出并做资源的回收或者重启操作)此类似于Android的异常捕获。
      • 通常对于根协程及其内部的所有子协程,通常子协程的异常交给其父协程,其父协程交给其父协程并到最终的根协程,如果异常始终未被捕获则会交给根协程的handler去处理。可捕获异常不会交给最终的handler去处理。
      • 在监控(supervision)作用范围内运行的协程, 不会将异常传播到它的父协程, 因此属于上述规则的例外情况.


        kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时),第16张
        不可捕获异常
    • 可捕获异常:
      • 协程构建器async创建的协程出现异常被成为可捕获异常,此异常要求调用者来处理最终的异常,比如使用 await 或 receive 来处理异常。


        kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时),第17张
        可捕获异常
    • 取消异常
      • 先前已提到协程取消后在协程中停止执行且抛出取消异常,这些异常会被所有的异常处理器忽略, 因此它们只能用来在 catch 块中输出额外的调试信息(即在协程中可以抓取取消异常并打印相关的信息)
      • 子协程的取消异常并不会影响其父协程。即子协程的取消不会取消父协程。但是子协程除了取消异常外的其他异常会影响到父协程,即其他异常在子协程停止的同时会让其父协程停止。


        kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时),第18张
        子协程的取消传递实例
      • 由上可知针对未捕获异常,子协程的异常只有在所有的子协程都停止的状态下才会交由指定的handler去处理。


        kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时),第19张
        多子协程异常的处理
    • 异常的聚合:针对多个子协程的协程,其异常只是处理最初的异常,即第一个异常被处理,其余的异常会被覆盖掉。取消异常除外。


      kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时),第20张
      协程异常的聚合
  5. 协程中的子协程:
    • 协程中可以启动多个子协程,通常子协程的上下文继承与父协程,并且新协程的 Job 会成为父协程的任务的一个 子任务. 当父协程被取消时, 它所有的子协程也会被取消, 并且会逐级递归, 取消子协程的子协程.
    • 如果启动协程时明确指定了当不同的作用范围(比如, GlobalScope.launch), 那么协程不会从父协程继承 Job.
    • 如果传递了不同的 Job 对象作为新协程的 context 参数(参见下面的示例程序), 那么这个参数会覆盖父 scope 的 Job
    • 父协程总是会等待它的所有子协程运行完毕. 父协程不必明确地追踪它启动的子协程, 也不必使用 Job.join 来等待子协程运行完毕:
  6. 协程命名及其属性指定协程上下文环境
    • CoroutineName("v1coroutine"):为了方便调试协程,使用此函数可以方便的为协程指定名字,对协程来说, 上下文元素 CoroutineName 起到与线程名类似的作用. 当 调试模式 开启时, 协程名称会包含在正在运行这个协程的线程的名称内.
    • 有些时候我们会需要对协程的上下文定义多个元素. 这时我们可以使用 + 操作符. 比如, 我们可以同时使用明确指定的派发器, 以及明确指定的名称, 来启动一个协程:


      kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时),第21张
      协程的属性指定
  7. 监控任务:
    • 针对协程的异常,子协程的异常(取消异常除外)会传递到其他的子协程及其父协程,如果想要子协程的异常仅影响在自己的协程内部则可以使用监控任务(给创建的协程指定监控):
      • SupervisorJob 可以用作这类目的. 它与通常的 Job 类似, 唯一的区别在于取消只向下方传播. 我们用下面的示例程序来演示一下:


        kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时),第22张
        监控任务
      • 对于 带作用范围 的并发, 可以使用 supervisorScope 代替 coroutineScope 来实现同一目的. 它也只向一个方向传播取消, 并且只在它自身失败的情况下取消所有的子协程. 它在运行结束之前也会等待所有的子协程结束, 和 coroutineScope 一样.
        kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时),第23张
        监控域的使用
      • 监控任务中的异常处理:仅影响到自己协程,即子协程的异常不会传递到父协程,此时针对未捕获异常可以直接给这个协程添加handler去处理,即:


        kotlin协程1:协程的基本概念(包括挂起函数和协程的取消和超时),第24张
        监控任务异常处理

参考文章:
简书:详解协程一
协程作用域
kotlin密集型协程任务的取消几种方式
kotlin协程
kotlin协程基础


https://www.xamrdz.com/backend/3x21940106.html

相关文章: