Kotlin 之 协程
初识协程,启动取消协程,Flow异步流,协程并发
目录
- (一)初识协程
- 协程是什么?
- Android中协程解决了什么问题?
- 协程的挂起与恢复
- 挂起和阻塞
- 协程的调度器 Dispatchers
- 任务泄露
- 结构化并发
- MainScope使用案例:
- viewModelScope使用案例
- (二)启动取消协程
- 协程的构建器
- 协程的四种启动模式
- 协程的作用域构建器
- Job对象
- job的生命周期
- 协程的取消
- CPU密集型任务取消
- 如下是不包含挂起函数的密集型任务
- 协程取消的副作用
- use函数,比如我们需要读取txt文件
- 不能取消的任务
- 超时任务
- 协程的异常处理
- 取消与异常
- 异常聚合
- (三)Flow异步流
- flow介绍
- flow的应用场景
- 流构建器
- 流的取消
- 操作符
- map
- transform
- take
- 末端操作符:collect,tolist,toset,reduce,fold
- 展平操作符
- flatMapConcat
- flatMapLatest
- 流的异常处理
- 流的完成
- StateFlow
- SharedFlow
- (四)协程并发
- 认识channel
- produce 与actor
- channel的关闭
- BroadcastChannel
- 多路复用
- 复用多个await
- 复用多个channel
- SelectClause
- Flow实现多路复用
- 协程的并发安全
(一)初识协程
协程是什么?
- 协程基于线程,它是轻量级线程
- coroutine是cooperation(协作)和routine(日常)的简写
- 协程让异步逻辑同步化,杜绝回调地狱
- 协程的核心是:函数或一段程序被挂起,之后再在挂起的位置恢复
- 协程的实现分为基础设施层和业务框架层,类似java中的NIO和Netty
- 业务框架层指的是我们常用的协程函数
基础设施层(原生api)实现例子
private fun doContinuation() {//协程体val continuation = suspend {println("协程执行中...")"协程的返回值"}.createCoroutine(object : Continuation<String> {override fun resumeWith(result: Result<String>) {//回调println("协程执行结束: $result")}override val context: CoroutineContext = EmptyCoroutineContext})continuation.resume(Unit)}
如上代码使用的是:import kotlin.coroutines.*
而kotlin协程业务框架层使用的是:import kotlinx.coroutines.*
Android中协程解决了什么问题?
- 处理耗时任务,这种任务常常会阻塞主线程
- 保证主线程安全,确保安全的在主线程调用suspend函数
在Android 11 谷歌建议使用协程来替代异步任务(asynctask)
协程的挂起与恢复
常规函数包括:invoke(call)和return,协程新增了suspend和resume
suspend:挂起或暂停,表示暂停执行当前协程,并保存所有局部变量
resume:让已暂停的协程从暂停处恢复执行
使用 suspend关键字修饰的函数叫挂起函数
挂起函数只能在协程体内或其他挂起函数内调用
挂起和阻塞
挂起:挂起点先记录下来,然后去做耗时任务,做完以后再恢复
阻塞:不做别的事情,一直等待
private fun doGlobalScope() {//如果没有指定调度器,那么默认就是使用:Dispatchers.Default(default也是非主线程)GlobalScope.launch(Dispatchers.Main) {//挂起(不会阻塞主线程,比如按钮按下去会立马弹起来,然后6秒后打印)delay(6000)Log.v("zx", "${Thread.currentThread().name},挂起6秒后")}//阻塞(会阻塞主线程,比如按钮按下去要等5秒后才能弹起来)Thread.sleep(5000)Log.v("zx", "${Thread.currentThread().name},阻塞5秒后")}
主线程在遇到挂起点后,可以不用等待直接更新UI,但是遇到阻塞就必须等待阻塞完成之后
协程的调度器 Dispatchers
所有协程必须的调度器中运行
Dispatchers.Main:主线程,处理UI交互和轻量级任务(调用suspend,调用UI函数,更新liveData)
Dispatchers.IO:非主线程,为磁盘和网络IO进行了优化(数据库,文件读写,网络请求)
Dispatchers.Default:非主线程,专为CPU密集型任务进行了优化(数组排序,json解析,差异判断)
任务泄露
当某个协程任务丢失,导致内存,cpu资源浪费,称为任务泄露,为了避免协程泄露,Kotlin引入了结构化并发机制。
结构化并发的作用:取消任务,追踪任务,发送错误信号
结构化并发
定义协程,必须指定其CoroutineScope,它会跟踪所有协程,还可以取消由它所启动的所有协程。
常用的api有:
GlobalScope:生命周期是process级别的,即使Acitivty和fragment已经销毁,协程仍然在执行,GlobalScope是一个顶级协程,不太建议直接用
MainScope:在Activity中使用,可以在onDestroy中取消协程
viewModelScope:只能在viewModel中使用,绑定viewModel的生命周期
lifecycleScope:只能在Activity和fragment中使用,会绑定Activity和Fragment的生命周期
MainScope使用案例:
class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }private val mainScope = MainScope()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(binding.root)doMainScope()}private fun doMainScope() {//retrofit不用写withcontext(Dispatchers.IO)因为retrofit会自动侦察到,//如果你是挂起函数会自动启用协程,创建一个异步线程去做操作mainScope.launch {//Toast.makeText(this@MainActivity, "MainScope", Toast.LENGTH_SHORT).show()try {//retrofit请求delay(1000)} catch (e: Exception) {//调用 mainScope.cancel()取消协程会抛出异常e.printStackTrace()}}//MainActivity 继承了委托以后CoroutineScope by MainScope(),就可以直接使用launch了launch {}}override fun onDestroy() {super.onDestroy()mainScope.cancel()//class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {}//MainActivity 继承了委托以后CoroutineScope by MainScope(),就可以直接使用canclecancel()}
}
viewModelScope使用案例
class MyViewModel:ViewModel() {private val _count = MutableLiveData<Int>()val count: LiveData<Int>get() = _countfun getUser(){//因为继承了ViewModel,那么就可以直接使用viewModelScopeviewModelScope.launch {//如果是耗时操作的话,retrofit会自动起一个io线程来执行,可以省略withContext(Dispatchers.IO)delay(3000)_count .value = 123}}
}
调用
class MainActivity : AppCompatActivity() {private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }private val myViewModel by viewModels<MyViewModel>()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(binding.root)initViewModelAndLiveData()}private fun initViewModelAndLiveData() {myViewModel.count.observe(this, Observer {binding.btnRandom.text = it.toString()})myViewModel.getUser()}
}
(二)启动取消协程
协程的构建器
launch和async构建器都用来启动新协程
launch,返回一个job并且不附带任何结果值
async,返回一个Deferred,Deferred也是一个job,可以使用.await()在一个延期的值上得到它的最终结果
//等待一个作业:join与awaitprivate fun runBlocking1(){//runBlocking可以把主线程变成一个协程//job1和job2是runBlocking的子协程//runBlocking会等待job1和job2这两个子协程执行完毕,会阻塞主线程(阻塞:按钮按下不会立马弹起job1和job2执行完了才会弹起)runBlocking {val job1 = launch {delay(2000)Log.v("zx", "job1 to finish")}val job2 = async {delay(2000)Log.v("zx", "job2 to finish")"job2 value"}//await可以得到返回值val job2Result = job2.await()Log.v("zx", "job2的返回值:$job2Result")}
需求:等待job1执行完毕以后再执行job2和job3
如果通过launch来启动的话,用join函数
如果通过async来启动的话,用await函数
//join和await都是挂起函数,不会阻塞主线程//如果通过launch来启动的话,用join函数runBlocking {val job1 = launch {delay(2000)Log.v("zx", "job1 to finish")}//这个函数会等待job1执行完后才会执行后面的job1.join()val job2 = launch {delay(100)Log.v("zx", "job2 to finish")}val job3 = launch {delay(100)Log.v("zx", "job3 to finish")}}//如果通过async来启动的话,用await函数runBlocking {val job1 = async {delay(2000)Log.v("zx", "job1 to finish2")}//这个函数会等待job1执行完后才会执行后面的job1.await()val job2 = async {delay(100)Log.v("zx", "job2 to finish2")}val job3 = async {delay(100)Log.v("zx", "job3 to finish2")}}}
需求:前面2个任务相加的结果给第三个任务(async结构化并发)
//runBlocking 在主线程中,子协程会继承父协程的上下文//runBlocking是Dispatchers.Main中启动的,doOne和doTwo也会使用父协程的调度器Dispatchers.Main中启动private fun runBlocking2() {//前面2个任务相加的结果给第三个任务(async结构化并发)runBlocking {val time = measureTimeMillis {//同步的val one = doOne()val two = doTwo()Log.v("zx", "数据${one + two}")}Log.v("zx", "time = $time")}runBlocking {val time = measureTimeMillis {//异步的val one = async { doOne() }val two = async { doTwo() }Log.v("zx", "数据${one.await() + two.await()}")//下面这种写法是错误的//val one2 = async { doOne() }.await()//val two2 = async { doTwo() }.await()//Log.v("zx","数据${one2+two2}")}Log.v("zx", "asynctime = $time")}}private suspend fun doOne():Int{delay(1000)return 1}private suspend fun doTwo():Int{delay(1000)return 2}
协程的四种启动模式
CoroutineStart.DEFAULT: 协程创建后立即开始调度,调度前如果协程被取消,则执行取消
CoroutineStart.ATOMIC: 协程创建后立即开始调度,协程执行到第一个挂起点之前不响应取消
CoroutineStart.LAZY: 协程被需要时,包括主动调用协程的start,join,await等函数时才会开始调度,如果调度前被取消,则协程进入异常结束状态
CoroutineStart.UNDISPATCHED: 协程创建后立即在当前函数栈中执行,直到遇到第一个真正挂起的点
private fun runBlocking3(){//runBlocking会等待所有子协程全部执行完runBlocking {val job1 = launch(start = CoroutineStart.DEFAULT) {delay(3000)Log.v("zx","finished")}delay(1000)//CoroutineStart.DEFAULT则会被取消job1.cancel()val job11 = launch(start = CoroutineStart.ATOMIC) {//delay就是第一个挂起函数,delay这里就是第一个挂起点,// 如果没执行到第一个挂起点之前取消,ATOMIC是不响应取消的delay(3000)Log.v("zx","finished")}delay(1000)job11.cancel()val job2 = async(start = CoroutineStart.LAZY) {20}delay(2000)//调度前被取消,那么进入异常状态job2.cancel()//如果是launch就用join启动,如果是async就用start或await启动Log.v("zx","job2 ${job2.await()}")//如何实现使用Dispatchers.IO,你的协程仍然在主线程里面?//答:使用CoroutineStart.UNDISPATCHED,因为当前函数runBlocking在主线程//DISPATCHED是转发,UNDISPATCHED的意思是不转发(在主线程创建的协程,就在主线程执行)//UNDISPATCHED是立即执行,而其他的是立即调度,立即调度不代表立即执行//立即在当前函数栈中执行,当前函数栈就是在主线程中val job3 = async(context = Dispatchers.IO, start = CoroutineStart.UNDISPATCHED) {Log.v("zx","当前${Thread.currentThread().name}")}}}
协程的作用域构建器
coroutineScope和runBlocking
区别是
runBlocking是常规函数,而coroutineScope是挂起函数,他们都会等待子协程执行结束
runBlocking会阻塞当前线程来等待
coroutineScope只是挂起,会释放底层线程用于其他用途
coroutineScope:一个协程失败了,所有其他兄弟协程也会被取消
supervisorScope:一个协程失败了,不会影响其他兄弟协程
coroutineScope:一个协程失败了,所有其他兄弟协程也会被取消
private fun runBlocking4(){//结构化并发,CoroutineScope(作用域构建器)runBlocking{//协程作用域,coroutineScope一定要等待job1和job2这两个子协程执行完毕,//coroutineScope继承的父协程的协程作用域coroutineScope {val job1 = launch {delay(500)Log.v("zx", "job1 to finish")}val job2 = async {delay(100)Log.v("zx", "job2 to finish")"job2 value"throw NullPointerException()}}}}
supervisorScope:一个协程失败了,不会影响其他兄弟协程
private fun runBlocking4(){//结构化并发,CoroutineScope(作用域构建器)runBlocking{//协程作用域,coroutineScope一定要等待job1和job2这两个子协程执行完毕,//coroutineScope继承的父协程的协程作用域supervisorScope {val job1 = launch {delay(500)Log.v("zx", "job1 to finish")}val job2 = async {delay(100)Log.v("zx", "job2 to finish")"job2 value"throw NullPointerException()}}}}
Job对象
- 每个创建的协程(通过launch或async)会返回一个job实例,该实例是协程的唯一标识,并负责管理协程的生命周期
- 一个任务可以包含一系列状态:新创建(New),活跃(Active),完成中(completing),已完成(completed),取消中(Canceling),已取消(Cancelled),虽然我们无法直接访问这些状态,但是我们可以访问job的属性,isActive,isCanceled和isCompleted
job的生命周期
如果协程处于活跃状态,协程运行出错或者调用job.cancel()都会将当前任务置为取消中(isActive = false isCanceled = true),当所有子协程都完成后,协程会进入已取消状态(isCanceled = true),此时isCompleted = true
协程的取消
- 取消作用域会取消它的子协程
- 被取消的子协程并不会影响其他兄弟协程
- 协程通过抛出CancellationException来处理取消操作
- 所有kotlinx.coroutines中的挂起函数(withcontext,delay等)都是可取消的
runBlocking {//CoroutineScope自己构建一个协程作用域,不继承runBlocking父协程的上下文val scope = CoroutineScope(Dispatchers.Default)val job1 = scope.launch {try {delay(1000)Log.v("zx", "job1")} catch (e: Exception) {e.printStackTrace()}}val job2 = scope.launch {delay(1000)Log.v("zx", "job2")}delay(100)//这里取消作用域,那么子协程就会被取消//被取消的子协程并不会影响其他兄弟协程,所以job2打印出来了//job1.cancel()//自定义取消异常job1.cancel(CancellationException("我取消了"))//这里会先打印,runBlocking不会等待CoroutineScope里面的子协程执行完毕Log.v("zx", "runBlocking")}打印:
com.z.zjetpack V/zx: runBlocking
com.z.zjetpack W/System.err: java.util.concurrent.CancellationException: 我取消了
com.z.zjetpack V/zx: job2
CPU密集型任务取消
isActive是一个可以使用在CoroutineScope的拓展属性,检查job是否处于活跃状态
ensureActive():如果job处于非活跃状态,这个方法会立即抛出异常
yield函数会检查所在协程状态,如果已经取消则抛出CancellationException予以响应。它还会尝试让出线程执行权,给其他协程提供执行机会。(如果此任务特别抢占系统资源,那么可以使用yield)
如下是不包含挂起函数的密集型任务
runBlocking {val startTime = System.currentTimeMillis()val job1 = launch(Dispatchers.Default) {var nextPrintTime = startTimevar i = 0while (i < 5) {//每隔0.5秒打印一次if (System.currentTimeMillis() > nextPrintTime) {Log.v("zx", "i = ${i++}")nextPrintTime += 500}}}Log.v("zx", "等待取消")delay(1000)//因为不存在suspend关键字的挂起函数,所以无法取消//job1.cancel()//job1.join()//等同于上方2个方法,为什么要用join,join是等待的意思,执行cancel()方法后,不会立马取消而是进入cancelling,//即取消中,所以join方法是等待取消中变为取消完成。job1.cancelAndJoin()Log.v("zx", "取消中")}打印:
com.z.zjetpack V/zx: 等待取消
com.z.zjetpack V/zx: i = 0
com.z.zjetpack V/zx: i = 1
com.z.zjetpack V/zx: i = 2
com.z.zjetpack V/zx: i = 3
com.z.zjetpack V/zx: i = 4
com.z.zjetpack V/zx: 已取消
可以发现,我们调用了cancelAndJoin去执行取消,最终的结果是并没有取消,那么这种密集型任务怎么取消呢?
while (i < 5 && isActive)
while (i < 5) {ensureActive()...
while (i < 5) {yield()...
打印:
com.z.zjetpack V/zx: 等待取消
com.z.zjetpack V/zx: i = 0
com.z.zjetpack V/zx: i = 1
com.z.zjetpack V/zx: i = 2
com.z.zjetpack V/zx: 已取消
上面3种方式都可以取消。
协程取消的副作用
- 在finally种释放资源
- use 函数:该函数只能被实现了Closeable的对象使用,程序结束时会自动调用close方法,适合文件对象。
因为协程取消了就会抛出异常,那么下面的代码就不会执行了,下面的代码有可能要释放资源,那么下面的代码不执行了,也就不会释放资源了,比如IO操作等,那么怎么处理呢?
答:在finally种释放资源,不管取不取消,finally代码块都会执行
runBlocking {val job1= launch {try {repeat(10) {Log.v("zx","sleep")delay(1000)}}finally {//不管取不取消这里都会执行//取消后会抛出异常,不影响我释放资源Log.v("zx","释放资源")}}delay(2000)job1.cancel()}
use函数,比如我们需要读取txt文件
普通写法:
private fun read(){val input = assets.open("1.txt")val br = BufferedReader(InputStreamReader(input))with(br) {var line: String?try {while (true) {line = readLine() ?: breakLog.v("zx","数据$line")}} finally {close()}}}
use写法
private fun readUse(){val input = assets.open("1.txt")val br = BufferedReader(InputStreamReader(input))with(br) {var line: String?use {while (true) {line = readLine() ?: breakLog.v("zx","数据$line")}}}}
不能取消的任务
- 处于取消中的协程不能够挂起,当协程被取消后想要调挂起函数,需要放在withContext(NonCancellable) 中,这样会挂起运行中的代码并保持取消中状态,直到任务处理完成。
例子:
runBlocking {val job1 = launch {try {repeat(10) {Log.v("zx", "sleep")delay(1000)}} finally {Log.v("zx", "开始sleep")delay(1000)Log.v("zx", "结束sleep")}}delay(2000)job1.cancel()}
打印:
com.z.zjetpack V/zx: sleep
com.z.zjetpack V/zx: sleep
com.z.zjetpack V/zx: 开始sleep
可以发现结束sleep永远不会打印出来,那怎么办呢?
使用 withContext(NonCancellable)
runBlocking {val job1 = launch {try {repeat(10) {Log.v("zx", "sleep")delay(1000)}} finally {//如果想要协程的取消不影响这里调用挂起函数,那么需要用到 withContext(NonCancellable) // 长驻任务也可以用这个withContext(NonCancellable) {Log.v("zx", "开始sleep")delay(1000)Log.v("zx", "结束sleep")}}}delay(2000)job1.cancel()}
打印:
com.z.zjetpack V/zx: sleep
com.z.zjetpack V/zx: sleep
com.z.zjetpack V/zx: 开始sleep
com.z.zjetpack V/zx: 结束sleep
超时任务
- 很多请求取消协程的理由是它有可能超时
- withTimeoutOrNull通过返回null来进行超时操作,从而替代抛出一个异常
例子
runBlocking {//需要在1秒内处理完withTimeout(1000) {repeat(10) {Log.v("zx", "sleep")delay(500)}}}
如上,如果在一秒内没处理完,那么就会抛出异常 kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1000 ms
那么如果网络请求在1秒内没返回,我们不想抛出异常,只想返回个默认值怎么办呢?
那么如果我们不想抛出异常,只想返回个null值的情况,该怎么做呢?
答:使用withTimeoutOrNull
runBlocking {//1秒内处理完,如果在1秒内没处理完,那么就返回null来代替抛出异常val result = withTimeoutOrNull(1000) {repeat(10) {Log.v("zx", "sleep")delay(500)}"完成"} ?: "默认数据"Log.v("zx", "结果:$result")}
打印:
com.z.zjetpack V/zx: sleep
com.z.zjetpack V/zx: sleep
com.z.zjetpack V/zx: 结果:默认数据
如上,如果在1秒内完成了,那么结果为 完成,如果没做完会返回结果null,为null即显示默认数据
协程的异常处理
runBlocking {val job1 = launch {try {throw NullPointerException()} catch (e: Exception) {log("launch,$e.toString()")}}val job2 = async {try {throw NullPointerException()} catch (e: Exception) {log("async,$e.toString()")}}job2.await()}
异常的传播特性是:当一个协程生成异常,它会传给它的父级,之后父级会取消它自己的子级,然后取消它自己,最后将异常传给它的父级。
那么如果我们想要一个子协程发送异常不影响其他协程怎么办呢?
答:使用SupervisorJob和SupervisorScope
使用SupervisorJob时,一个子协程的运行失败不会影响到它的子协程。SupervisorJob不会传播异常给它的父级,他会让子协程自己处理异常
使用CoroutineExceptionHandler捕获协程异常
val handle = CoroutineExceptionHandler { coroutineContext, throwable ->Log.v("zx","$throwable")}CoroutineScope(Dispatchers.Main).launch(handle) {throw NullPointerException()}
Android种全局异常处理
全局异常处理器可以获取到所有协程未处理的未捕获异常,不管它并不能对异常进行捕获,虽然不能阻止程序崩溃,全局异常处理器在程序调试和异常上报场景有很大作用。
我们需要在app/src/main下面创建一个resources/META-INF/services目录并在其中创建一个名为kotlinx.coroutines.CoroutineExceptionHandler的文件,文件内容就是异常处理器的全类名。
package com.z.zjetpack.coroutineimport android.util.Log
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlin.coroutines.CoroutineContextclass GException:CoroutineExceptionHandler {override val key = CoroutineExceptionHandleroverride fun handleException(context: CoroutineContext, exception: Throwable) {Log.v("zx","异常信息:$exception")}
}
kotlinx.coroutines.CoroutineExceptionHandler文件中的内容为:包名+类名
com.z.zjetpack.coroutine.GException
取消与异常
- 取消与异常紧密相关,协程内部使用CancellationException来取消,这个异常会被忽略,当子协程被取消时,不会取消它的父协程
runBlocking {val job = launch {val childjob = launch {try {delay(Long.MAX_VALUE)}finally {Log.v("zx","子协程被取消了")}}//出让执行权,让子协程有机会执行yield()Log.v("zx","开始取消")childjob.cancelAndJoin()//childjob.cancel()//没有AndJoin就会继续往下执行//Log.v("zx","取消中。。。")yield()Log.v("zx","父协程还没被取消")//父协程中释放资源...}job.join()}打印:
com.z.zjetpack V/zx: 开始取消
com.z.zjetpack V/zx: 子协程被取消了
com.z.zjetpack V/zx: 父协程还没被取消
- 如果一个协程遇到了CancellationException以外的异常,它将使用该异常取消它的父协程。当父协程的所有子协程都结束后,异常才会被父协程处理。
runBlocking {val handle = CoroutineExceptionHandler { coroutineContext, throwable ->Log.v("zx","捕获异常:$throwable")}val job1 = GlobalScope.launch(handle) {val child1 = launch {try {delay(Long.MAX_VALUE)}finally {//这里如果要执行挂起函数要用NonCancellablewithContext(NonCancellable) {Log.v("zx","child1子协程已被取消,但异常未被处理")delay(100)Log.v("zx","child1子协程已完成")}}}val child2 = launch {delay(10)Log.v("zx","child2 抛出异常")throw NullPointerException()}}job1.join()}打印:
com.z.zjetpack V/zx: child2 抛出异常
com.z.zjetpack V/zx: child1子协程已被取消,但异常未被处理
com.z.zjetpack V/zx: child1子协程已完成
com.z.zjetpack V/zx: 捕获异常:java.lang.NullPointerException
异常聚合
当协程的多个子协程因为异常而失败时,一般取第一个异常处理,在第一个异常后发生的所有异常都会绑定到第一个异常上。
runBlocking {val handle = CoroutineExceptionHandler { coroutineContext, throwable ->Log.v("zx", "其他异常:${throwable.suppressed.contentToString()}")Log.v("zx", "当前捕获异常:$throwable")}GlobalScope.launch(handle) {launch {try {delay(Long.MAX_VALUE)} finally {throw NullPointerException()}}launch {try {delay(Long.MAX_VALUE)} finally {throw IndexOutOfBoundsException()}}launch {delay(100)throw ArithmeticException()}}}打印:其他异常:[java.lang.NullPointerException, java.lang.IndexOutOfBoundsException]当前捕获异常:java.lang.ArithmeticException
(三)Flow异步流
flow介绍
挂起函数可以异步返回单个值,那如何异步多次返回多个值呢?
使用flow,flow的特点:
- flow{…}块中的代码可以挂起
- 使用flow,suspend修饰符可以省略
- 流使用emit函数发射值
- 流使用collect的函数收集值
- flow类似冷流,flow中代码直到流被收集(调用collect)的时候才运行,类似lazy,什么时候用,什么时候执行。
- 流的连续性:流收集都是按顺序收集的
- flowOn可更改流发射的上下文,即可以指定在主线程或子线程中执行
- 与之相对的是热流,我们即将介绍的 StateFlow 和 SharedFlow 是热流,在垃圾回收之前,都是存在内存之中,并且处于活跃状态的。
//使用flow,suspend修饰符可以省略fun doflow() = flow<Int> {for (i in 1..5) {//这里是挂起,不是阻塞delay(500)emit(i)}}.flowOn(Dispatchers.IO)
//调用runBlocking {doflow().collect {log("value=$it")}}
打印(多次返回多个值)
com.z.zjetpack V/zx: value=1
com.z.zjetpack V/zx: value=2
com.z.zjetpack V/zx: value=3
com.z.zjetpack V/zx: value=4
com.z.zjetpack V/zx: value=5
flow的应用场景
文件下载场景
//正在下载(文件总大小为5)fun doflow() = flow<Double> {for (i in 1..5) {delay(500)emit(i.toDouble())}//flowOn来指定在IO线程中下载}.flowOn(Dispatchers.IO)
//读取进度runBlocking {doflow().collect {log("当前下载=${it / 5 * 100}%")}}打印:
com.z.zjetpack V/zx: 当前下载=20.0%
com.z.zjetpack V/zx: 当前下载=40.0%
com.z.zjetpack V/zx: 当前下载=60.0%
com.z.zjetpack V/zx: 当前下载=80.0%
com.z.zjetpack V/zx: 当前下载=100.0%
流构建器
flowof 和asflow
runBlocking {flowOf(1, 2, 3).onEach { delay(500) }.collect {log("value = $it")}(5..8).asFlow().onEach { delay(500) }.collect {log("value = $it")}}
使用launchin替换collect在单独的协程中启动收集流。
fun event() = (1..3).asFlow().onEach {delay(500)}.flowOn(Dispatchers.IO)//调用runBlocking {val job = event().onEach {log("value = $it")}.launchIn(CoroutineScope(Dispatchers.IO))//主线程可用this//.launchIn(this)job.join()}
流的取消
超时的时候取消
fun cancelFlow() = flow<Int> {for (i in 1..5) {delay(1000)emit(i)}}//调用runBlocking {//超时的时候取消流withTimeoutOrNull(2500) {cancelFlow().collect {log("value = $it")}}}打印:在2.5秒的时候超时了,取消了
com.z.zjetpack V/zx: value = 1
com.z.zjetpack V/zx: value = 2
直接取消
runBlocking {cancelFlow().collect {log("value = $it")if(it == 3){cancel()}}}
繁忙的任务是不能直接取消的,需要检测取消(cancellable)
runBlocking {(1..5).asFlow().cancellable().collect {if(it == 3) {cancel()}}}
背压:生产者效率 > 消费者效率
使用缓冲和flowon来处理背压
buffer():并发运行流中发射元素的代码
conflate():合并发射项,不对每个值处理
collectLatest():取消并重新发送最后一个值
模拟背压代码:
fun preFlow() = flow<Int> {for (i in 1..5) {delay(100)emit(i)log("发送$i")}}//调用//100ms发送一次,300ms接收一次就产生了背压runBlocking {val time = measureTimeMillis {preFlow()//buffer可以增加缓冲,提高效率//.buffer(100)//flowOn自带缓冲功能//.flowOn(Dispatchers.IO)//conflate不对每个值处理//.conflate()//.collect//取消并重新发送最后一个值.collectLatest {delay(300)log("接收到:$it")}}log("总耗时 $time")}
打印:
com.z.zjetpack V/zx: 接收到:1
com.z.zjetpack V/zx: 发送1
com.z.zjetpack V/zx: 接收到:2
com.z.zjetpack V/zx: 发送2
com.z.zjetpack V/zx: 接收到:3
com.z.zjetpack V/zx: 发送3
com.z.zjetpack V/zx: 接收到:4
com.z.zjetpack V/zx: 发送4
com.z.zjetpack V/zx: 接收到:5
com.z.zjetpack V/zx: 发送5
com.z.zjetpack V/zx: 总耗时 2033使用buffer后
com.z.zjetpack V/zx: 发送1
com.z.zjetpack V/zx: 发送2
com.z.zjetpack V/zx: 发送3
com.z.zjetpack V/zx: 接收到:1
com.z.zjetpack V/zx: 发送4
com.z.zjetpack V/zx: 发送5
com.z.zjetpack V/zx: 接收到:2
com.z.zjetpack V/zx: 接收到:3
com.z.zjetpack V/zx: 接收到:4
com.z.zjetpack V/zx: 接收到:5
com.z.zjetpack V/zx: 总耗时 1634使用flowOn后
com.z.zjetpack V/zx: 发送1
com.z.zjetpack V/zx: 发送2
com.z.zjetpack V/zx: 发送3
com.z.zjetpack V/zx: 接收到:1
com.z.zjetpack V/zx: 发送4
com.z.zjetpack V/zx: 发送5
com.z.zjetpack V/zx: 接收到:2
com.z.zjetpack V/zx: 接收到:3
com.z.zjetpack V/zx: 接收到:4
com.z.zjetpack V/zx: 接收到:5
com.z.zjetpack V/zx: 总耗时 1639使用conflate后
com.z.zjetpack V/zx: 发送1
com.z.zjetpack V/zx: 发送2
com.z.zjetpack V/zx: 发送3
com.z.zjetpack V/zx: 接收到:1
com.z.zjetpack V/zx: 发送4
com.z.zjetpack V/zx: 发送5
com.z.zjetpack V/zx: 接收到:3
com.z.zjetpack V/zx: 接收到:5
com.z.zjetpack V/zx: 总耗时 1034使用collectLatest后
com.z.zjetpack V/zx: 发送1
com.z.zjetpack V/zx: 发送2
com.z.zjetpack V/zx: 发送3
com.z.zjetpack V/zx: 发送4
com.z.zjetpack V/zx: 发送5
com.z.zjetpack V/zx: 接收到:5
com.z.zjetpack V/zx: 总耗时 843
操作符
转换操作符:map ,transform
限长操作符:取指定数量,take
末端操作符:末端操作符用于启动流收集的挂起函数,collect,tolist,toset,reduce,fold
组合操作符:zip
展平操作符:flatMapConcat(连接),flatMapMerge(合并),flatMapLatest(最新)
map
suspend fun perRequest(req: Int): String {delay(1000)return "转换 $req"}runBlocking {(1..3).asFlow().map {perRequest(it)}.collect {log(it)}}
打印:
com.z.zjetpack V/zx: 转换 1
com.z.zjetpack V/zx: 转换 2
com.z.zjetpack V/zx: 转换 3
transform
runBlocking {(5..6).asFlow().transform {emit("s $it")emit(perRequest(it))emit("e $it")}//.take(4).collect {log(it)}}
打印:
com.z.zjetpack V/zx: s 5
com.z.zjetpack V/zx: 转换 5
com.z.zjetpack V/zx: e 5
com.z.zjetpack V/zx: s 6
com.z.zjetpack V/zx: 转换 6
com.z.zjetpack V/zx: e 6
take
加上take之后
com.z.zjetpack V/zx: s 5
com.z.zjetpack V/zx: 转换 5
com.z.zjetpack V/zx: e 5
com.z.zjetpack V/zx: s 6
末端操作符:collect,tolist,toset,reduce,fold
runBlocking {val sum = (1..5).asFlow().map { it * it }.reduce { a, b -> a + b }log("sum = $sum")val nList = (1..5).asFlow().toList()log("nList = $nList")val nSet = listOf(1, 2, 2, 3, 3, 5).asFlow().toSet()log("nSet = $nSet")}
打印:
com.z.zjetpack V/zx: sum = 55
com.z.zjetpack V/zx: nList = [1, 2, 3, 4, 5]
com.z.zjetpack V/zx: nSet = [1, 2, 3, 5]
展平操作符
只使用map的时候
//返回值是一个flowfun reqFlow(i: Int) = flow<String> {emit("start $i")delay(500)emit("end $i")}runBlocking {(0..1).asFlow().map {reqFlow(it)}.collect {log("首次collect = $it")it.collect {log("二次 = $it")}}}
打印:由于返回是flow所以需要collect 两次才能拿到值,Flow<Flow<String>>
com.z.zjetpack V/zx: 首次collect = kotlinx.coroutines.flow.SafeFlow@63db1bf
com.z.zjetpack V/zx: 二次 = start 0
com.z.zjetpack V/zx: 二次 = end 0
com.z.zjetpack V/zx: 首次collect = kotlinx.coroutines.flow.SafeFlow@d27108c
com.z.zjetpack V/zx: 二次 = start 1
com.z.zjetpack V/zx: 二次 = end 1
flatMapConcat
runBlocking {(0..1).asFlow().flatMapConcat {reqFlow(it)}.collect {log("首次collect = $it")}}
打印:直接展开了
com.z.zjetpack V/zx: 首次collect = start 0
com.z.zjetpack V/zx: 首次collect = end 0
com.z.zjetpack V/zx: 首次collect = start 1
com.z.zjetpack V/zx: 首次collect = end 1
runBlocking {(0..1).asFlow().flatMapMerge {reqFlow(it)}.collect {log("首次collect = $it")}}
打印:
com.z.zjetpack V/zx: 首次collect = start 0
com.z.zjetpack V/zx: 首次collect = start 1
com.z.zjetpack V/zx: 首次collect = end 0
com.z.zjetpack V/zx: 首次collect = end 1
flatMapLatest
runBlocking {(0..1).asFlow().flatMapLatest {reqFlow(it)}.collect {log("首次collect = $it")}}打印:
com.z.zjetpack V/zx: 首次collect = start 0
com.z.zjetpack V/zx: 首次collect = start 1
com.z.zjetpack V/zx: 首次collect = end 1
流的异常处理
catch函数 和 try catch
flow {emit(1)throw NullPointerException()//catch函数只捕获上游的异常}.catch {log("exception $it")//在异常后恢复emit(20)}.flowOn(Dispatchers.IO).collect {log("msg $it")}
打印:
com.z.zjetpack V/zx: exception java.lang.NullPointerException
com.z.zjetpack V/zx: msg 1
com.z.zjetpack V/zx: msg 20
//不建议通过这种方式捕获上游的异常,违反了flow原则,这种适合捕获下游的异常try {(1..3).asFlow().collect {check(it > 2) {"ex $it"}}} catch (e: Exception) {log("异常 $e")}
打印:
com.z.zjetpack V/zx: 异常 java.lang.IllegalStateException: ex 1
流的完成
finally 和 onCompletion
try {(1..3).asFlow().collect {check(it > 2) {"ex $it"}}} catch (e: Exception) {log("异常 $e")} finally {log("流已完成")}//发生异常onCompletion可以拿到异常信息,但不会捕获try {(1..3).asFlow().onCompletion {log("onCompletion $it")}.collect {check(it > 2) {"ex $it"}}} catch (e: Exception) {log("异常 $e")}
打印:
com.z.zjetpack V/zx: 异常 java.lang.IllegalStateException: ex 1
com.z.zjetpack V/zx: 流已完成
com.z.zjetpack V/zx: onCompletion java.lang.IllegalStateException: ex 1
com.z.zjetpack V/zx: 异常 java.lang.IllegalStateException: ex 1
StateFlow
StateFlow 是一个状态容器式可观察数据流,可以向其收集器发出当前状态更新和新状态更新。
- StateFlow使用
第一步:创建 MutableStateFlow 并设置初始化的值。
class MainViewModel : ViewModel() {val selected = MutableStateFlow<Boolean>(false)
}
第二步:同 Flow 一样,使用 collect 方法:
lifecycleScope.launch {viewModel.selected.collect {// ... 引起UI发生的变化// 比如 某个按钮是否选中状态}
}
第三步:可以给 selected设置值,从而引起 Ui 层的变化:
class MainViewModel : ViewModel() {val selected = MutableStateFlow<Boolean>(false)fun doSomeThing(value: Boolean) {selected.value = value}
}
普通的 Flow,是不具备 selected.value = value 这种能力的
StateFlow 和 LiveData 有什么区别?
有两点区别:
第一点,StateFlow 必须有初始值,LiveData 不需要。
第二点,当 View 变为 STOPPED 状态时,LiveData.observe() 会自动取消注册使用方,而从 StateFlow 或任何其他数据流收集数据则不会取消注册使用方。
对于 StateFlow 在界面销毁的时仍处于活跃状态,有两种解决方法:
使用 ktx 将 Flow 转换为 LiveData。
在界面销毁的时候,手动取消(这很容易被遗忘)。
class LatestNewsActivity : AppCompatActivity() {...// Coroutine listening for UI statesprivate var uiStateJob: Job? = nulloverride fun onStart() {super.onStart()// Start collecting when the View is visibleuiStateJob = lifecycleScope.launch {latestNewsViewModel.uiState.collect { uiState -> ... }}}override fun onStop() {// Stop collecting when the View goes to the backgrounduiStateJob?.cancel()super.onStop()}
}
SharedFlow
SharedFlow:数据共享,有点类似广播
和 StateFlow 一样,SharedFlow 也是热流,它可以将已发送过的数据发送给新的订阅者,并且具有高的配置性。
- SharedFlow使用场景
总的来说,SharedFlow 和 StateFlow 类似,他们都是热流,都可以用来存储状态,但 SharedFlow 配置灵活。
当你有如下场景时,需要使用 SharedFlow:
发生订阅时,需要将过去已经更新的n个值,同步给新的订阅者。
配置缓存策略。
2. SharedFlow的使用
简单写一个 Demo吧。
第一步:创建一个 MutableSharedFlow,对应的参数解释在注释中
class MainViewModel : ViewModel() {val sharedFlow = MutableSharedFlow<Int>(5 // 参数一:当新的订阅者Collect时,发送几个已经发送过的数据给它, 3 // 参数二:减去replay,MutableSharedFlow还缓存多少数据, BufferOverflow.DROP_OLDEST // 参数三:缓存策略,三种 丢掉最新值、丢掉最旧值和挂起)
}
第二步:使用emit或者tryEmit方法
class MainViewModel : ViewModel() {val sharedFlow = MutableSharedFlow<Int>(// ....)// 初始化时调用init {for (i in 0..10) {sharedFlow.tryEmit(i)}}// 在按钮中调用fun doAsClick() {for (i in 11..20) {sharedFlow.tryEmit(i)}}
}
当 MutableSharedFlow 中缓存数据量超过阈值时,emit 方法和 tryEmit 方法的处理方式会有不同:
emit 方法:当缓存策略为 BufferOverflow.SUSPEND 时,emit 方法会挂起,直到有新的缓存空间。
tryEmit 方法:tryEmit 会返回一个 Boolean 值,true 代表传递成功,false 代表会产生一个回调,让这次数据发射挂起,直到有新的缓存空间。
第三步:接收数据
接收数据的方式,跟普通的 Flow 没什么区别。
下面是我的全部代码:
class MainActivity : AppCompatActivity() {private lateinit var viewModel: MainViewModeloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)viewModel = ViewModelProvider(this).get(com.example.coroutinedemo.viewmodel.MainViewModel::class.java)val tvContent = findViewById<TextView>(R.id.tv_content)// 启动第一个协程,接收初始化的数据lifecycleScope.launch {val sb = StringBuffer()viewModel.sharedFlow.collect {sb.append("<<${it}")tvContent.text = sb}}val btnGo = findViewById<Button>(R.id.btn_go)val tvTwo = findViewById<TextView>(R.id.tv_2)btnGo.setOnClickListener {// 发送新的数据viewModel.doAsClick()// 发送新的数据以后,启动第二个协程lifecycleScope.launch {val sb = StringBuffer()viewModel.sharedFlow.collect {sb.append("<<${it}")tvTwo.text = sb.toString()}}}}
}
- 将冷流转化为SharedFlow
直接使用官网的代码,方法是使用 Flow 的扩展方法 shareIn:
class NewsRemoteDataSource(...,private val externalScope: CoroutineScope,
) {val latestNews: Flow<List<ArticleHeadline>> = flow {...}.shareIn(externalScope,replay = 1,started = SharingStarted.WhileSubscribed() // 启动政策)
}
重点是参数三,分别提供了三个启动策略:
SharingStarted.WhileSubscribed():存在订阅者时,将使上游提供方保持活跃状态。
SharingStarted.Eagerly:立即启动提供方。
SharingStarted.Lazily:在第一个订阅者出现后开始共享数据,并使数据流永远保持活跃状态。
总结
Flow 给我的感觉就像古老的印刷术,版面定了就不可更改,不过,该版面可印刷多张内容;StateFlow 给我的感觉就像活字印刷,可以不停的更改版面,也可以使用同一个版面印刷很多内容。
如果你要使用 Flow 记录数据的状态,StateFlow 和 SharedFlow 会是一个不错的选择。StateFlow 和 SharedFlow 提供了在 Flow 中使用 LiveData 式更新数据的能力,但是如果要在 UI 层使用,需要注意生命周期的问题。
StateFlow 和 SharedFlow 相比,StateFlow 需要提供初始值,SharedFlow 配置灵活,可提供旧数据同步和缓存配置的功能。
协程进阶技巧 - StateFlow和SharedFlow
(四)协程并发
认识channel
channel是一个并发安全的队列,可以连接协程,实现不同协程的通信。
Library中定义了几种类型的Channel。 它们在内部能够存储多种元素,只是在send调用是否能够挂起方面有所不一样。 对于全部通道类型,receive调用的行为方式相同:若是通道不为空,则接收元素,不然将挂起。
Unlimited channel
无限制通道(Unlimited channel)是最接近队列的模拟:生产者能够将元素发送到此通道,而且它将无限增加。 send方法将永远不会被挂起。 若是没有更多的内存,则会抛出OutOfMemoryException。 和队列不一样的是当使用者尝试从空通道接收消息并被挂起直到有一些新元素发送到该通道时继续使用。
Buffered channel
缓冲通道(Buffered channel)的大小受指定数字的限制。 生产者能够将元素发送到此通道,直到达到最大限制。 全部元素都在内部存储。 通道已满时,下一个send呼叫将被挂起,直到出现更多可用空间。
Rendezvous channel
"约定"通道(Rendezvous channel)是没有缓冲区的通道。 这与建立大小为零的缓冲通道(Buffered channel)相同。 其中一个功能(send或receive)始终被挂起,直到调用另外一个功能为止。 若是调用了send函数,但消费者没有准备好处理该元素则receive会挂起,而且send也会被挂起。 一样,若是调用了receive函数且通道为空,换句话说,没有准备好发送该元素的的send被挂起-receive也会被挂起。
Conflated channel
发送到合并通道( Conflated channel)的新元素将覆盖先前发送的元素,所以接收方将始终仅能获取最新元素。 send调用将永远不会被挂起。
建立通道时,指定其类型或缓冲区大小(若是须要缓冲的通道):
val rendezvousChannel = Channel<String>()
val bufferedChannel = Channel<String>(10)
val conflatedChannel = Channel<String>(CONFLATED)
val unlimitedChannel = Channel<String>(UNLIMITED)
默认状况下,会建立一个"约定"通道(Rendezvous channel)。
在如下示例中,将建立一个"约定"通道,两个生产者协程和一个消费者协程:
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.*fun main() = runBlocking<Unit> {val channel = Channel<String>()launch {channel.send("A1")channel.send("A2")log("A done")}launch {channel.send("B1")log("B done")}launch {repeat(3) {val x = channel.receive()log(x)}}
}fun log(message: Any?) {println("[${Thread.currentThread().name}] $message")
}以上将会打印以下结果:[main @coroutine#4] A1
[main @coroutine#4] B1
[main @coroutine#2] A done
[main @coroutine#3] B done
[main @coroutine#4] A2
channel实际上是一个队列,队列中一定存在缓冲区,这个缓冲区满了并且一直没有人调用receive取走函数,send就需要挂起,故意让接收端的节奏放慢,发现send总是被挂起,直到receive之后才会继续往下执行。
fun run1() {val channel = Channel<Int>(Channel.UNLIMITED)//Channel协程间通信,并发安全的队列runBlocking {//生产者val p = launch {for (i in 1..5) {channel.send(i)log("send = $i")}}//消费者val c = launch {//正常接收数据while (true) {//故意让接收端的节奏放慢,发现send总是被挂起,直到receive之后才会继续往下执行delay(2000)val el = channel.receive()log("re = $el")}//通过迭代器iterator接收数据//val iterator = channel.iterator()//while (iterator.hasNext()) {// delay(2000)// log("iterator = ${iterator.next()}")//}}joinAll(p,c)}}打印:
com.z.zjetpack V/zx: send = 1
com.z.zjetpack V/zx: send = 2
com.z.zjetpack V/zx: send = 3
com.z.zjetpack V/zx: send = 4
com.z.zjetpack V/zx: send = 5
com.z.zjetpack V/zx: re = 1
com.z.zjetpack V/zx: re = 2
com.z.zjetpack V/zx: re = 3
com.z.zjetpack V/zx: re = 4
com.z.zjetpack V/zx: re = 5
produce 与actor
- 构造生产者与消费者的便捷方法
- 我们可以通过produce方法启动一个生产者协程,并返回一个reveive channel,其他协程就可以用这个channel来接收数据了。反过来我们可以用actor启动一个消费者协程。
fun run2(){runBlocking {//快捷创建生产者协程,返回一个接收Channelval receiveChannel = produce<Int> {repeat(5){delay(1000)send(it)}}val job2 = launch {for (i in receiveChannel) {log("receiveChannel = $i")}}job2.join()}runBlocking {//构造消费者的便捷方法val sendChannel = actor<Int> {while (true) {val re = receive()log("re = $re")}}val p = launch {for (i in 1..3) {sendChannel.send(i)}}p.join()}}打印:
com.z.zjetpack V/zx: receiveChannel = 0
com.z.zjetpack V/zx: receiveChannel = 1
com.z.zjetpack V/zx: receiveChannel = 2
com.z.zjetpack V/zx: receiveChannel = 3
com.z.zjetpack V/zx: receiveChannel = 4
com.z.zjetpack V/zx: re = 1
com.z.zjetpack V/zx: re = 2
com.z.zjetpack V/zx: re = 3
channel的关闭
- produce和actor返回的channel都会随着对应的协程执行完毕而关闭,也正式这样,channel才会被称为热数据流.
- 对于一个channel,如果我们调用了它的close方法,它会立即停止接收新元素,它的isClosedForSend会立即返回true,由于channel缓冲区的存在,可能还有一些元素没有被处理完,所以要等所有元素都被读取之后isClosedForReceive才会返回true
- channel的生命周期最好由主导方来维护,建议由主导的一方实现关闭。
fun run3(){runBlocking {val channel = Channel<Int>(3)//生产者launch {List(3){channel.send(it)log("send = $it")}channel.close()log("isClosedForSend = ${channel.isClosedForSend}")log("isClosedForReceive = ${channel.isClosedForReceive}")}//消费者launch {for (c in channel) {log("re = $c")delay(1000)}log("消费isClosedForSend = ${channel.isClosedForSend}")log("消费isClosedForReceive = ${channel.isClosedForReceive}")}}}打印:
com.z.zjetpack V/zx: send = 0
com.z.zjetpack V/zx: send = 1
com.z.zjetpack V/zx: send = 2
com.z.zjetpack V/zx: isClosedForSend = true
com.z.zjetpack V/zx: isClosedForReceive = false
com.z.zjetpack V/zx: re = 0
com.z.zjetpack V/zx: re = 1
com.z.zjetpack V/zx: re = 2
com.z.zjetpack V/zx: 消费isClosedForSend = true
com.z.zjetpack V/zx: 消费isClosedForReceive = true
BroadcastChannel
发送端和接收端在channel中存在一对多的场景,虽然有多个接收端,但是同一个元素只会被一个接收端读取到,广播则不同,多个接收端不存在互斥行为。
fun run4() {runBlocking {//直接创建// val broadcastChannel = BroadcastChannel<Int>(Channel.BUFFERED)//broadcast方法创建val channel = Channel<Int>()val broadcastChannel = channel.broadcast(Channel.BUFFERED)//创建3个协程来接收List(3) {launch {val receiveChannel = broadcastChannel.openSubscription()for (r in receiveChannel) {log("协程 $it, re = $r")}}}launch {List(3) {broadcastChannel.send(it)}broadcastChannel.close()}}}打印:
com.z.zjetpack V/zx: 协程 0, re = 0
com.z.zjetpack V/zx: 协程 0, re = 1
com.z.zjetpack V/zx: 协程 0, re = 2
com.z.zjetpack V/zx: 协程 1, re = 0
com.z.zjetpack V/zx: 协程 1, re = 1
com.z.zjetpack V/zx: 协程 1, re = 2
com.z.zjetpack V/zx: 协程 2, re = 0
com.z.zjetpack V/zx: 协程 2, re = 1
com.z.zjetpack V/zx: 协程 2, re = 2
多路复用
复用多个await
两个api分别从网络和本地获取数据,期望哪个先返回就先用哪个做显示
fun CoroutineScope.getFromLocal() = async {delay(1000)"返回本地数据"}fun CoroutineScope.getFromNet() = async {"返回网络数据"}fun run5() {runBlocking {launch {val local = getFromLocal()val net = getFromNet()val res = select<String> {local.onAwait { it }net.onAwait { it }}log("值 = $res")}.join()}}打印:
com.z.zjetpack V/zx: 值 = 返回网络数据
复用多个channel
跟await类似,会接收到最快的那个channel消息
fun run6() {runBlocking {val channels = listOf(Channel<Int>(), Channel<Int>())launch {delay(100)channels[0].send(1)}launch {delay(500)channels[1].send(5)}val result = select<Int> {channels.forEach { re ->re.onReceive{it}}}log("result = $result")}}
打印:
com.z.zjetpack V/zx: result = 1
SelectClause
哪些事件可以被select?SelectClause类型
包括:
SelectClause0:对应事件没有返回值,例如 join 没有返回值,对应的 onJoin 就是这个类型,使用时 onJoin 的参数是一个无参函数:
public val onJoin: SelectClause0runBlocking {val job1 = launch {delay(100)log("job1")}val job2 = launch {delay(10)log("job2")}select<Unit> {job1.onJoin {log("job1.onJoin")}job2.onJoin {log("job2.onJoin")}}}打印:
com.z.zjetpack V/zx: job2
com.z.zjetpack V/zx: job2.onJoin
com.z.zjetpack V/zx: job1
SelectClause1:对应事件有返回值,前面的 onAwait 和 onReceive 都是此类情况。
public val onAwait: SelectClause1<T>public val onReceive: SelectClause1<E>
SelectClause2:对应事件有返回值,此外还需要额外的一个参数,例如 Channel.onSend 有两个参数,第一个就是一个 Channel 数据类型的值,表示即将发送的值,第二个是发送成功时的回调。
如果我们想要确认挂起函数是否支持select,查看是否存在对应的SelectClauseN类型可回调即可。
//返回SelectClause2public val onSend: SelectClause2<E, SendChannel<E>>runBlocking {val channels = listOf(Channel<Int>(), Channel<Int>())launch {select<Unit> {launch {delay(100)channels[0].onSend(1) { sendChannel ->log("send on $sendChannel")}}launch {delay(500)channels[1].onSend(5) { sendChannel ->log("send on $sendChannel")}}}}launch {for (c in channels) {log("数据 = ${c.receive()}")}}}
打印:
com.z.zjetpack V/zx: send on RendezvousChannel@63db1bf{EmptyQueue}
com.z.zjetpack V/zx: 数据 = 1
Flow实现多路复用
coroutineScope {val login = "..."listOf(::getUserFromApi, ::getUserFromLocal) ... ①.map { function ->function.call(login) ... ②}.map { deferred ->flow { emit(deferred.await()) } ... ③}.merge() ... ④.onEach { user ->println("Result: $user")}.launchIn(this)
}
这其中,① 处用创建了两个函数引用组成的 List;② 处调用它们得到 deferred;③ 处比较关键,对于每一个 deferred 我们创建一个单独的 Flow,并在 Flow 内部发送 deferred.await() 返回的结果,即返回的 User 对象;现在我们有了两个 Flow 实例,我们需要将它们整合成一个 Flow 进行处理,调用 merge 函数即可。
协程的并发安全
除了线程中常用的解决并发安全问题的手段外,协程提供了一些并发安全的工具
- channel:并发安全的消息通道
- Mutex:轻量级锁,lock和unlock和线程锁类似,轻量级是说它在获取不到锁时不会阻塞线程而是挂起等待锁的释放。
- Semaphore:轻量级信号量,信号量可以有多个,协程在获取到信号量后即可执行并发操作,当Semaphore的参数为1时,效果等同于Mutex
fun run7() {runBlocking {var count = 0List(10000) {//GlobalScope是线程不安全的GlobalScope.launch {count++}}.joinAll()log("默认count = $count")}//使用volatile解决并发问题runBlocking {var count = AtomicInteger(0)List(10000) {//GlobalScope是线程不安全的GlobalScope.launch {count.incrementAndGet()}}.joinAll()log("volatile count = ${count.get()}")}//使用Mutex解决并发问题runBlocking {var count = 0var mutex = Mutex()List(10000) {//GlobalScope是线程不安全的GlobalScope.launch {mutex.withLock {count++}}}.joinAll()log("Mutex count = $count")}//使用Semaphore解决并发问题runBlocking {var count = 0var semaphore = Semaphore(1)List(10000) {//GlobalScope是线程不安全的GlobalScope.launch {semaphore.withPermit {count++}}}.joinAll()log("Semaphore count = $count")}}
打印:
com.z.zjetpack V/zx: 默认count = 9991
com.z.zjetpack V/zx: volatile count = 10000
com.z.zjetpack V/zx: Mutex count = 10000
com.z.zjetpack V/zx: Semaphore count = 10000
除了使用这些工具解决并发问题,也可以避免访问外部可变状态,编写函数时,要求它不得访问外部状态,只能基于入参做运算,通过返回值提供运算结果。
runBlocking {var count = 0//count在协程外面不存在并发问题val result = count + List(10000){GlobalScope.async { 1 }}.map {it.await()}.sum()log("count count = $result")}
来自:你能听懂的Kotlin协程课
更多:
一文看透 Kotlin 协程本质
Kotlin之协程coroutine场景推荐使用(1)
Kotlin 之 协程相关推荐
- Kotlin的协程与生命周期
文章目录 一.前言 二.引入依赖 三.代码示例 1.基础用法 2.repeatOnLifecycle 3.flowWithLifecycle 4.lifecycle.whenCreated.lifec ...
- 十一、kotlin的协程(一)
theme: Chinese-red 学习的前提 java线程需要大概知道点 协程是线程执行的任务, 协程和用户线程的区别在于, 协程背靠强大的编译器, 协程有专属于协程的调度器和一堆方便好用的函数, ...
- Kotlin中协程理解与实战(一)
Kotlin中协程理解与实战(一) 什么是协程 在Android中协程用来解决什么问题 协程是: suspend -也称为挂起或暂停,用于暂停执行当前协程,并保存所有局部变量: resume -用于让 ...
- Kotlin高级协程
Kotlin高级协程 一.前言 二.先从线程说起 三.协程的设计思想 四.协程特点:优雅的实现移步任务 五.协程基本使用 六.协程和线程相比有什么特点,如何优雅的实现异步任务 一.前言 在文章正式上干 ...
- 【对比Java学Kotlin】协程简史
文章目录 一.概念释义 1.1 协程定义 1.2 与线程的关系 1.3 协程简史 二.种类划分 2.1 按调用栈分类 2.2 按调度方式分类 三.异步编程 3.1 多线程 3.2 回调 3.3 Pro ...
- Kotlin(3)-协程和操作符重载,Java程序员秋招三面蚂蚁金服
Kotlin 文件和类不存在一对一关系 共生体 继承 修饰符 空指针问题 正文 重难点 协程 想了很久,关于协程的内容,在官网上确实有很多内容,基础知识概念,基本使用,以及 流操作,通道,异常处理,并 ...
- kotlin coroutines 协程教程-入门用法
kotlin coroutines 协程教程-入门用法 Coroutine 协程,是kotlin 上的一个轻量级的线程库,对比 java 的 Executor,主要有以下特点: 更轻量级的 api 实 ...
- 十一、kotlin的协程 - 缓存、volatile、内存屏障和cas(四) --- 跑题篇
本章写着写着就跑题了, 又不舍得删除, 新手看 # 协程的共享变量安全问题简单入门和## volatile 不保证原子性部分代码, 其他可以不看, 太乱, 也没用 协程的共享变量安全问题简单入门 在使 ...
- 忘记Rxjava吧,你应该试试Kotlin的协程
0.前言 协程以前一直是Kotlin作为实验性的一个库,前些日子发现1.3版本的kotlin relese了协程,所以就找时间研究了一下,本来早就想写这篇文章了,但是因为离职换工作的原因,迟迟未能动笔 ...
最新文章
- 什么是区块链智能合约?
- MySQL中的RAND()函数使用详解
- WebSocket部署服务器外网无法连接解决方案
- 26 进程优先级队列Queue
- solr 配置mysql数据源_solr data-config.xml配置文件的见解mysql数据源
- python list存储方式_Python 之 将 list 存储为 .mat 文件
- mybatis-plus3.5.1学习笔记
- mailbox 编程_往死里写——从站mailbox实现 | 学步园
- 慎用PHP $_REQUEST数组
- 159.Oracle数据库SQL开发之 SQL优化——若干
- 从五个维度来谈谈视觉设计师如何阐述设计风格
- 解决Docker 一直starting 的办法
- 自己的联想Y450笔记本无法连接无线网络的解决办法
- 跟零计算机基础的房东女儿讲了一下午的中间人劫持京东事件后,她感激涕零,决定给我免除房租...
- macOS SwiftUI 指示器组件规范之 02 评级指标 Rating Indicators
- 歌单详情内容-播放列表 (音乐app项目-第8步)
- 全媒体运营师胡耀文教你:社群运营这6种裂变模型,你掌握了吗?
- 【C语言】浮点型数据在内存中的存储方式
- 自我鉴定范文 php,毕业生优秀自我鉴定
- 图像的表示方法和种类
热门文章
- 若依-vue图片上传本地改OSS前台以及后台-附带oss图片上传工具类
- 双目测距系列(七)monodepth2训练前数据集准备过程的简析
- idea中安装redis插件以及使用指南
- 数据仓库笔记(高质量建模)——01
- MUI+HTML5+Plus 拍照或者相册选择图片并上传服务器
- Google Earth Engine——ESRI_Global-LULC_10m全球地表覆盖度10米分辨率数据集(重磅消息最新上映!所有数据6G),内含下载代码!
- Windows下快速(快捷)打开service服务应用
- 毕业设计-基于协同过滤的课程资源定制推荐系统
- 嵌入式转行进公司的第三天
- 手把手教你了解Asp.net web窗体