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
-
思路:
-
声明变量 = suspend关键字{}
- 定义协程回调结果
-
使用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) } -
运行结果:
-
-
具体创建: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 }) } -
运行截图:
-
-
源码分析: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方法,会触发协程启动?
-
协程启动代码:
-
查看createCoroutine源码:
-
发现协程体的返回值是SafeContinuation的实例
-
-
查看SafeContinuation源码:
-
SafeContinuation中有一个delegate属性,其指代某个匿名内部类
- delegate实际上是拦截器拦截后的结果
-
-
这个内部类其实就是协程体
-
协程体:suspend修饰的一段Lamda表达式
-
Kotlin语言决定的,suspend Lamda编译后生成的匿名内部类继承自SuspendLamda
-
并且实现了Continuation接口,这个接口里面就有resumeWith方法
-
-
为什么Lamda表达式会编译成匿名内部类
-
Suspend Lamda中有一个抽象函数invokeSuspend
-
在其父类BaseContinuationImpl中声明了的
-
编译生成的匿名内部类中这个函数的实现就是协程体
-
-
-
-
协程体的Receiver
-
作用域分类:
- 顶级作用域:没有父协程的协程所在的作用域
- 协同作用域:协程中启动新协程(即子协程),此时子协程所在的作用域默认为协同作用域,子协程抛出的未捕获异常都将传递给父协程处理,父协程同时也会被取消;
- 主从作用域:与协同作用域父子关系一致,区别在于子协程出现未捕获异常时不会向上传递给父协程
-
作用域有什么用?
- 协程必须在协程作用域中才能启动,
- 协程作用域中定义了一些父子协程的规则,
- Kotlin 协程通过协程作用域来管控域中的所有协程
-
添加作用域有什么用?
-
可以提供函数支持
- 协程体内部可以调用作用域外定义的函数
-
函数限制:协程体内部就不能调用作用域外定义的函数了(delay)
- 在协程作用域定义上加上注解@RestrictsSuspension
- 避免一些无效甚至是危险的挂起函数,例如API这个的序列生成器(Squence Builder)
-
-
协程体添加作用域
-
未添加作用域:
-
协程的创建
-
协程的启动:
-
-
添加了作用域
-
协程的创建:
-
协程的启动:
-
-
-
封装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() } -
运行结果:
-
-
可以被挂起的main函数
-
背景:
- Kotlin1.3,main可以被声明为挂起函数;
- suspend main即可
-
出现原因:
- Kotlin 从程序入口获得协程,程序都将在此协程体中运行
-
工作原理:
-
一定是做了特殊处理
- 因为协程是语言特性,虚拟机无法理解协程
-
Kotlin 编译器帮助生成了一个main函数,在其中调用了runSuspend函数并执行main逻辑
- 反编译查看suspend main
-
-