Kotlin协程初探(二)

562 阅读7分钟

Kotlin协程初探(二):协程的基本概念

协程究竟是什么?

  • 核心点:讨论程序流程的控制

  • 工作表现:

    • 函数或者一段程序能被挂起,稍后在挂起位置恢复
    • 挂起与恢复由程序自己控制,实现流程协作调度
  • 协程与线程的区别:

    • 从任务角度

      • 线程:一旦开始,不会停止以抢占式连续执行任务,一般不存在协作关系
      • 协程:通过挂起恢复实现彼此协作
    • 从实现角度来看:

      • 线程:主流OS中拥有成熟的线程模型,应用层中的线程就是这个,属于操作系统的概念;线程调度由OS决定

        • 补充虚拟机对线程的支持:

          • Android虚拟机--->pthread
        • Java Object类下的wait方法:支持了各种锁的实现,底层为Condition

      • 协程:某些编程语言的一种特性(规范的味道),Java可以借助Quasar框架实现

协程的分类:按调用栈分类

  • 调用栈是什么?

    • 一般值函数调用栈,保存函数调用状态的数据结构
    • 狭义:普通函数调用栈
    • 广义:能保存调用状态就行
  • 为什么需要调用栈?

    • 协程需要支持挂起与恢复,将挂起点状态保存在栈中
    • Tips:线程由CPU调度而中断,其中断状态也会保存在栈中;
  • 有栈协程:

    • 是什么?

      • 每一个协程都有自己的调用栈,类似于线程;区别在于具体的调度
    • 优点:可以在任意函数调用层级的任意位置挂起,并转移调度权

      • 类似于Lua的协程
    • 缺点:为协程开辟一块栈内存(以MB为单位)

      • 优化例子:go routime

        • 在运行时根据需要,以内存页(4KB)进行扩、缩容
  • 无栈协程:

    • 是什么?

      • 协程没有自己的调用栈,挂起点的状态由状态机或者闭包等语法实现
    • 优点:不会开辟栈内存

    • 缺点:挂起位置受限

      • 类似于Python中的Generator

      • 优化例子:Kotlin协程

        • 流程控制依靠协程体变一变生成的状态机的状态流转实现,变量保存由闭包语法实现

        • 在挂起函数范围内的任意层级挂起

          • 启动Kotlin协程,在其中任意嵌套suspend函数

             suspend fun fun0(){
                 fun1()
             }
             suspend fun fun1(){
                 ……
             }
                 ……
             suspend fun funx(){
                 ……
             }
            
  • Kotlin协程挂起细节:suspend使用细节

    • 代码展示:

       suspend fun fun0(){
           fun1()//甲
       }
       suspend fun fun1(){
           funx()//乙
       }
           
       suspend fun funx(){
           ……
       }
      
    • 在甲处是伪挂起,当乙处调用后才挂起fun1;

      • Kotlin 协程的挂起需要suspend的嵌套
  • Kotlin 协程挂起细节:为什么不从语言角度改进,直接将普通函数定义为挂起函数?

    • 牵一发动全身:所有Kotlin所支持的运行环境( JVM,Node.js)都要改

    • 导致协程切换称为函数本身特性:编码难度加大,类似于go routime

      • 避免隐式调度:保留yield,resume;但不实用
    • Kotlin 平衡了对运行环境的依赖与在任意函数上挂起

协程的分类:按对称性分类

  • 对称协程:任意协程相互独立且平等,调动权可以相互切换

    • 接近线程:go routime

      • 读写不同channel来实现控制权转移
  • 非对称协程:协程只能向其调用者出让调度权,协程间存在调用与被调关系

    • 接近思维:常用语言大多都是这样

      • lua协程:当前协程以yield,将调度权交给其调用者

      • async/await:

        • await:调度权转交给异步调用,异步调用结果或异常将调用权交还await
    • 非对称--->对称

      • 设计中立调度权转交中心,根据参数转交调度权

        • lua:coro第三方框架
        • kotlin:基于Channel的通信

Kotlin 协程的基础设施

Kotlin 协程实现分为两个层次

  • 基础设施层:标准库中的API

    • 简单协程
  • 业务框架层:协程的上层框架支持

    • 复合协程

协程的构造:

  • 协程的创建方式:

    • createCoroutine

      • 协程创建后并不会立即执行,需要搭配协程的启动
      • 返回值类型:Continuation
    • startCoroutine

      • 创建之后立即跑
      • 返回值类型:没有定义返回值类型
  • 具体创建:createCoroutine

    • 思路:

      1. 声明变量 = suspend关键字{}

        • 定义协程回调结果
      2. 使用createCorotine

        • 重写resumeWith
        • 重写context
    • 代码:

       import kotlin.coroutines.*
       ​
       fun main(){
           println("创建协程")
           val continuation = suspend {
               println("现在在协程体中")
               5//回调
           }.createCoroutine(object : Continuation<Int>{
               override fun resumeWith(result: Result<Int>) {
                   println("Cotoutine End $result")
               }
       ​
               override val context = EmptyCoroutineContext
           })
       ​
           //启动协程
           continuation.resume(Unit)
       }
      
    • 运行结果:

      image-20220505212703261

  • 具体创建:startCoroutine

    • 代码:

       import kotlin.coroutines.*
       ​
       fun main(){
           println("创建协程")
           val continuation = suspend {
               println("现在在协程体中")
               5//回调
           }.startCoroutine(object : Continuation<Int>{
               override fun resumeWith(result: Result<Int>) {
                   println("Cotoutine End $result")
               }
       ​
               override val context = EmptyCoroutineContext
           })
       }
      
    • 运行截图:

      图片.png

  • 源码分析:createCoroutine函数

    • 代码示意:

       public fun <T> (suspend () -> T).createCoroutine(
           completion: Continuation<T>
       ): Continuation<Unit> =
           SafeContinuation(createCoroutineUnintercepted(completion).intercepted(), COROUTINE_SUSPENDED)
      
    • suspend () -> T:createCoroutine函数的Receiver

      • Receiver:被suspend修饰的挂起函数,也就是协程执体(协程体)
      • completion:协程执行完后调用,协程的完成回调
      • Continuation:协程的返回值,需要通过这个启动协程
  • 协程的启动:为什么调用协程体返回值的resume方法,会触发协程启动?

    • 协程启动代码:

      image-20220505213258585

    • 查看createCoroutine源码:

      • 发现协程体的返回值是SafeContinuation的实例

        image-20220505213811726

    • 查看SafeContinuation源码:

      • SafeContinuation中有一个delegate属性,其指代某个匿名内部类

        image-20220505213944345

        • delegate实际上是拦截器拦截后的结果
    • 这个内部类其实就是协程体

      • 协程体:suspend修饰的一段Lamda表达式

        image-20220505214145779

      • Kotlin语言决定的,suspend Lamda编译后生成的匿名内部类继承自SuspendLamda

        • 并且实现了Continuation接口,这个接口里面就有resumeWith方法

          image-20220505214419676

      • 为什么Lamda表达式会编译成匿名内部类

        • Suspend Lamda中有一个抽象函数invokeSuspend

          • 在其父类BaseContinuationImpl中声明了的

            image-20220505215006536

          • 编译生成的匿名内部类中这个函数的实现就是协程体

协程体的Receiver

  • 作用域分类:

    • 顶级作用域:没有父协程的协程所在的作用域
    • 协同作用域:协程中启动新协程(即子协程),此时子协程所在的作用域默认为协同作用域,子协程抛出的未捕获异常都将传递给父协程处理,父协程同时也会被取消;
    • 主从作用域:与协同作用域父子关系一致,区别在于子协程出现未捕获异常时不会向上传递给父协程
  • 作用域有什么用?

    • 协程必须在协程作用域中才能启动,
    • 协程作用域中定义了一些父子协程的规则,
    • Kotlin 协程通过协程作用域来管控域中的所有协程
  • 添加作用域有什么用?

    • 可以提供函数支持

      • 协程体内部可以调用作用域外定义的函数
    • 函数限制:协程体内部就不能调用作用域外定义的函数了(delay)

      • 在协程作用域定义上加上注解@RestrictsSuspension
      • 避免一些无效甚至是危险的挂起函数,例如API这个的序列生成器(Squence Builder)
  • 协程体添加作用域

    • 未添加作用域:

      • 协程的创建

        image-20220505220444287

      • 协程的启动:

        image-20220505220536701

    • 添加了作用域

      • 协程的创建:

        image-20220505220643063

      • 协程的启动:

        image-20220505220807653

  • 封装launchCoroutine

    • 为什么要封装

      • Kotlin中没有直接声明带有Receiver的Lamda表达式
    • 怎么封装

      • 代码:

         fun <R,T> launchCoroutine(receiver: R,block:suspend R.() -> T ){
             block.startCoroutine(receiver,object : Continuation<T>{
                 override fun resumeWith(result: Result<T>) {
                     println("Coroutine End:$result")
                 }
                 override val context = EmptyCoroutineContext
             })
         }
        
    • 怎么启动:

      • 代码:

         //启动封装后的协程体
             class ProducerScope<T>{
                 suspend fun produce(value:T){
                     println("定义作用域")
                 }
             }
             fun callLauchCoroutine(){
                 launchCoroutine(ProducerScope<Int>()){
                     println("处于协程体")
                     produce(1024)
         ​
                 }
             }
         ​
             callLauchCoroutine()
        
    • 完整代码:

       import kotlin.coroutines.*
       ​
       fun main(){
           println("封装launchCoroutine")
           fun <R,T> launchCoroutine(receiver: R,block:suspend R.() -> T ){
               block.startCoroutine(receiver,object : Continuation<T>{
                   override fun resumeWith(result: Result<T>) {
                       println("Coroutine End:$result")
                   }
                   override val context = EmptyCoroutineContext
               })
           }
           //启动封装后的协程体
           class ProducerScope<T>{
               suspend fun produce(value:T){
                   println("定义作用域")
               }
           }
           fun callLauchCoroutine(){
               launchCoroutine(ProducerScope<Int>()){
                   println("处于协程体")
                   produce(1024)
               }
           }
       ​
           callLauchCoroutine()
       }
      
    • 运行结果:

      image-20220505222948783

  • 可以被挂起的main函数

    • 背景:

      • Kotlin1.3,main可以被声明为挂起函数;
      • suspend main即可
    • 出现原因:

      • Kotlin 从程序入口获得协程,程序都将在此协程体中运行
    • 工作原理:

      • 一定是做了特殊处理

        • 因为协程是语言特性,虚拟机无法理解协程
      • Kotlin 编译器帮助生成了一个main函数,在其中调用了runSuspend函数并执行main逻辑

        • 反编译查看suspend main