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

59 阅读9分钟
  1. 什么是协程?
    • 项目中的需求特别是复杂的需求并不是单一任务的,可能是一个接一个任务组成的,特别是下一个任务需要上一个任务执行完成提供数据,此种情形被称为异步编程(需要注意的是此异步和我们平时说的通过线程实现异步不一样),此异步是代码阶段上说的异步编程,平时的代码通常都是顺序去写,顺序执行,但是此时由于任务场景(一个任务中小任务的串联)需要异步去写代码,java中通常使用Listener,Future,Rx去写,kotlin提供了协程的模式去写异步编程(简单说就是用顺序写代码实现异步的场景)

    • 协程与并发:线程是通过系统调度多线程同时执行逻辑实现并发提升响应速度等,协程则是将单一任务拆分多个小的任务,每个小任务声明为一个协程,通过协程的同时并发提升整体任务的执行时间,进而提升响应速度。

    • 协程和线程的区别:协程通常被称为轻量级的线程。

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

  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,下一个协程取决于上一个协程的执行线程。
  • newSingleThreadContext: 将会在独自的新线程中执行,一个专用的线程是一种非常昂贵的资源. 在真实的应用程序中, 这样的线程, 必须在不再需要的时候使用 close函数释放它, 或者保存在一个顶层变量中, 并在应用程序内继续重用
  • 如上所述:launch和async不传递参数(即不指定对应的执行线程)的时候,其执行线程将会继承与新建协程的作用域所在的线程。
  • withContext(ctx2):次函数上面也说了不会创建协程,但是其能够在一个协程中切换协程的执行线程。
  • coroutineContext[Job]:通过次函数能够获取到此协程的对象信息及其是否激活的信息,在调试协程的时候可以用起打印log来方便调试协程。
    • CoroutineContext,协程上下文,是一些元素的集合,主要包括 Job 和 CoroutineDispatcher 元素,可以代表一个协程的场景。可以类同于Android的Context(制订了协程的执行环境)

    • Job,任务,封装了协程中需要执行的代码逻辑。Job 可以取消并且有简单生命周期,它有三种状态:isActive,isCompleted,isCancelled。Job 完成时是没有返回值的,如果需要返回值的话,应该使用 Deferred,它是 Job 的子类。

  1. 挂起函数:
    • 使用修饰符suspend标记的函数被称为挂起函数。

      • 挂起函数和普通函数的参数及其返回值语法一致

      • 挂起函数调用的时候会挂起协程且不会阻塞当前协程所在的线程,待此挂起函数执行完毕会自动重启协程进行后续的执行。

      • 挂起函数在协程中或者在另外一个挂起函数中,且协程中通常至少一个挂起函数。

      • 挂起修饰符suspend修饰符可以标记普通函数、扩展函数和lambda表达式。

    • 针对协程中的多挂起函数,其执行顺序即挂起函数的代码顺序。

    • 使用async在一个协程中修饰多个挂起函数则多个挂起函数可以并发执行:

      • 概念上来说, async 就好象 launch 一样. 它启动一个独立的协程, 也就是一个轻量的线程, 与其他所有协程一起并发执行。

      • 和launch不同的是 async返回的不是一个job 而是Deferred(轻量级的非阻塞的future)通过其可以获取到协程的最终返回值,且deferred是job的子类,同样可以取消。

      • async可以实现协程的并发执行,若多协程并发执行则必须显示声明。

    • 使用async (Lazily started) 实现协程的延迟执行,即协程在其父协程调用的时候就开始执行,若不想其此时执行而是程序员控制其执行的时刻,则可以通过此语法设置。此语法设置以后仅在调用job的start和await访问协程的结果的时候才会真正的去执行。

      • GlobalScope.async:不推荐在此作用域中使用async创建协程,因为在此作用域中当前协程出现异常并不能够协程取消而是继续执行。

      • 针对上面的逻辑使用 async 的结构化并发替代解决,即:

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

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

    • 有些时候我们会需要对协程的上下文定义多个元素. 这时我们可以使用 + 操作符. 比如, 我们可以同时使用明确指定的派发器, 以及明确指定的名称, 来启动一个协程:

👀关注公众号:Android老皮!!!欢迎大家来找我探讨交流👀