Kotlin协程初探(一)

1,051 阅读6分钟

协程概述:

  • 为什么需要协程,协程是什么?

    • 为什么需要协程:从操作系统发展历史角度入手:

      1. 真空管+穿孔卡片

        • 工作机制:

          • 程序进入输入室得到中间结果,在将中间结果放入输出室,得到最终结果
        • 问题:

          • CPU利用率极低,从而引入批处理系统
      2. 批处理+晶体管

        • 工作机制:收集所有程序写在磁带,将磁带放入计算机,可以使计算机宏观上满载工作
        • 问题:程序中存在IO操作时,会引起程序切换,降低性能(涉及到操作系统中断知识),此时引入多道程序
      3. 多道程序与集成电路

        • 工作机制:以进程为单位,降低工作粒度(一个程序通常对应一个进程);当进程1发生IO时,CPU可以处理其他进程(涉及到操作系统进程调度手段,RR调度(CPU轮转机制));但,这仍是一种消极手段并未主动解决程序中IO操作;此时引入,虚拟时间片轮转机制

        • 虚拟时间片轮转机制:优先处理IO操作

          • 内部维护一个队列,队列中存放存在IO操作的进程;当进程调度时,优先处理这部分存在IO操作的进程;

      • 由此可见,工作粒度降低可以实现程序性能优化;

        • 随着并发手段出现,工作粒度从进程--->线程--->协程;
    • 什么是协程:

      • 协程可以看作用户级线程(线程中的函数),是语言的一种特性区别与进程、线程等操作系统概念;Kotlin,golang语言中均有涉及

      • 工作机制:

        • 以协程为单位分配资源(内存、CPU时间)
        • 内部存在管理者:决定那个线程执行
        • 内部存在状态机:恢复CPU上下文,使得协程切出去还能切回来(异步对比 JVM中的程序计数器)

异步程序设计:

  • 概述:

    • Kotlin协程主要用于构建各类异步程序模型,由此探讨常见异步程序设计思路与模型;
  • 同步与异步:指令实际执行顺序与代码编写顺序

    • 同步:系统底层决定的

      • 程序在任何时候都是同步执行,即使发生中断也只是从一个程序切换到另外一个程序
    • 异步:有业务场景决定的

      • 代码编写顺序不能满足业务需求,例如延时操作等;
  • 并发与并行:

    • 并行:可以同时运行的进程数(两个咖啡机,两队同时使用)

      • 同时执行不同任务,实际上确实是同时执行
      • CPU逻辑核心数为8,那么并行度就是8,CPU可以同时执行8个进程(区别物理核心与逻辑核心)
    • 并发:不能脱离时间单位,只讨论单位时间的并发量,(例如,一分钟能提供几杯咖啡,两队人交替使用咖啡机,假如一分钟出了四杯咖啡,那么在这一分钟中,并发量为4)

      • 应用可以交替执行不同任务,看起来是同时执行(操作系统中部的技术,切得很快)
      • 实际上不是同时执行的,只是切得很快,让用户感觉不到
      • 时间片轮转机制就是并发的一种手段
    • 线程数量由代码与系统共同决定:

      • 代码角度:

        • 通过线程兴起方式,创建新的线程,理论上无上限
      • 系统角度:

        • Linux中一个进程最多开1000个线程,一个系统最多同时存在1024个文件描述符
        • Windows中一个进程最多开2000个线程
        • 使用数据库(MySQL):连接数一般在150-200,但是应用往往使用不止这个
        • 线程开多了,导致服务器崩溃
      • 虚拟机角度:

        • java的本质可简单看做方法调方法;方法封装为栈帧,存入虚拟机栈;而虚拟机栈是有大小的
  • Kotlin程序的异步性:

    • 异步代码不一定立即执行,并且不希望其阻塞主线程(ANR)

      • 可能存在耗时任务(IO操作,主动延时)
      • thread函数是Kotlin API中对 Java Thread类的封装,调用后默认立即执行线程
    • 代码:应该是ACB

       println("异步代码")
       val task = {
           println("C")
       }
       println("A")
       thread(block = task)
       println("B")
      
    • 执行结果:

图片.png

  • Kotlin 异步代码回调

    • 需要拿到异步任务的执行结果,通过回调手段通知调用者

    • 代码:

       var res = 0;
       println("异步回调")
       val callback = {
           println("异步任务的结果 $res")
       }
       val task1 = {
           res = 3 + 2;
           callback()
       }
       println("计算3+2")
       thread(block = task1)
       println("同步代码的结果 $res")
      
    • 运行结果:

图片.png

  • Kotlin 异步代码中的回调地狱

    • 概述:回调中不断嵌套代码并涉及多个线程间的切换;
    • 解决:生产者-消费者模型或者EventBus框架
  • Kotlin 异步代码结果传递:suspend关键字的设计思路

    • 异步调用是立即返回的,根据结果就绪状态区分被调用者业务逻辑
    • 结果未就绪:被调用者执行异步代码,结果就绪后通过回调传给调用者
    • 结果已就绪:被调用者通过回调传给调用者
  • Kotlin 异步代码中取消响应:

    • 协作手段,通过interrupt + 检测标志位实现
  • Kotlin 异步代码中的复杂分支:

    • 类比 Java中的常见并发工具类
  • Kotlin 协程解决的问题:异步逻辑同步化

    • 问题:在异步代码中处理多个异常,可能导致处理异常代码多次调用;

    • 解决手段:将异常进行合并,简化代码;

常见异步程序设计思路:

  • 引入原因:降低异步程序复杂度

    • 通过某种手段将异步回调流程与主流程整合,使得异步代码看起来像同步代码
  • Furture:

    • 引入时间:JDK1.5引入

    • 重要方法:get

      • 同步阻塞式返回Furture对应的异步任务结果;
    • 缺点:让异步不再是异步,需要在原地等待结果

    • 优化:引入CompletableFurture

  • CompletableFurture

    • 实现了Furture接口

    • get函数的调用仍在在异步环境中,不会阻塞主调用流程,但是让结果获取脱离主调用流程

    • 优化手段:

      • 自己去整个调用结果,并在合适时机返回
      • 引入Promise与async/await
  • Promise与async/await

    • Promise:一个异步任务,存在挂起、完成、拒绝三个状态

      • 完成状态:结果通过调用then方法的参数进行回调
      • 出现异常拒绝时:通过catch方法传入的参数来捕获拒绝的原因
      • Promise.all:将多个Promise整合在一起,then消费的就是这个结果
    • async:语法糖

      • 放在外部函数声明前,在Promise.all之前加上await;
      • 将语法转成我们比较熟悉的代码
  • 响应式编程:关注数据流的变换与流转,重点在数据输入与输出之间的关系

    • 工作机制:

      • 输入与输出之间用函数变换来链接,函数之间只对输入输出负责
      • 通过将这个函数分发到不同线程上来实现异步,类似于RxJava
    • Single与Observable

      • Single:

        • RxJava中提供的一个像Promise的东西
        • 只有一个结果
      • Observable:执行取决于订阅而不是立即执行

        • 提供了任意变换之间可以切换线程调度器的能力,可以让复杂的数据变换与流转实现异步;当然也导致滥用为线程切换的工具