热流
来看一下简单的 生产者消费者 模型:
fun main(){
GlobalScope.launch {
val channel= Channel<Int>()
val producer=GlobalScope.launch {
var i=0
while (true){
delay(1000)
channel.send(i++)
}
}
val consumer=GlobalScope.launch {
while (true){
val element=channel.receive()
println(element)
}
}
producer.join()
consumer.join()
}
Thread.sleep(10000)
}
复制代码
生产者 每隔一秒 发送一个值,消费者 一直读取这个值 并且打印 很简单的例子 但是背后的细节 极多,这里一定要好好掌握,热流绝非网上大部分人云亦云的文章 所说的那么简单,搞不清楚 很容易 使用的时候埋坑。
先来看一下ide 的表现:
红框标出来的很明显, 也就是说 channel的send和receive 都是 挂起函数 ,这是什么意思?
从上面的例子可以看见出来,因为发送端是延迟1s才发数据的,而 接受端 是一直在读数据,那么这里
发送端肯定会比 接收段慢,也就是说 当程序开始运行的时候,接收端 一直收不到数据 所以receivee函数 就 一直挂起在那里了,直到有新元素到达。
那么send函数 也是个挂起函数,send会不会也出现 一直挂起等待的情况呢?
send 会挂起吗?
还是刚才那个例子 我们稍微改一下:
GlobalScope.launch {
val channel= Channel<Int>()
val producer=GlobalScope.launch {
var i=0
while (true){
delay(1000)
i++
println("send $i")
channel.send(i)
}
}
val consumer=GlobalScope.launch {
while (true){
delay(2000)
val element=channel.receive()
println("receive $element")
}
}
复制代码
这一次 我们让 receive 比 send 慢一点, 看看send 会不会被挂起?
从图中很明显的能看出来是吧 send函数 总是要等到 receive了以后 自己才能执行。
问题出在哪呢》?看下我们的channel定义就可以了
其实这个默认值 RENDEZVOUS 意思就是 这个channel的缓冲区是0,讲白了就是不见不散的机制 只要你没receive 那我就不会send,一定是要我之前send出去的数据 你receive了以后 我才会发下一个
大家可以尝试一下 试试不同的参数 会有什么不同的效果
val channel= Channel<Int>(capacity = Channel.UNLIMITED)
复制代码
接收者的另外几种写法
这两种写法 可能更加直观一些,唯一要注意的时候 iterator的写法 她会在hasNext 那边就会挂起了 同样的 第二种写法 在in 语句的时候 就会挂起了
channnel的关闭
来看一个例子:
GlobalScope.launch {
val channel= Channel<Int>(3)
val producer=GlobalScope.launch {
List(3){
channel.send(it)
println("send $it")
}
channel.close()
println("close channel send: ${channel.isClosedForSend} receive:${channel.isClosedForReceive}")
}
val consumer=GlobalScope.launch {
for (element in channel){
println("receive $element")
delay(1000)
println("close channel send: ${channel.isClosedForSend} receive:${channel.isClosedForReceive}")
}
}
producer.join()
consumer.join()
}
复制代码
我们创建了一个缓存区为3个大小的 channel,然后生产者 发送3个数据以后,直接调用了channel的close 方法
消费者 每隔1s 收一条数据
看下执行结果可以看出来,receive 端 一定是等到全部接收完毕以后 整个channel才会真正的关闭的。
对于channel的使用, 了解到这个程度 应该说就足够了,还有一些 还处于实验性质的api 这里就不再一一介绍了
大家只要谨记:channel的使用 一定要在合适的时候 主动调用close,否则接收端 大概率一直挂起,虽然不会内存泄漏 但是总归是会占用一定的系统资源,虽然这个资源小到可以忽略不计,对于channel的close,我们只要谨记从发送端开始主导close就可以了,原因?
原因当然是:只有发才会有收吗,这是符合人类逻辑的 毕竟大家都是等人通知要给你送生日礼物 你才会去收的吗
此外,channel还有一个特点是 发送端并不依赖接收端,这与我们下面马上要介绍的flow 冷流是截然不同的
flow的初体验
fun main()= runBlocking{
val intFlow=flow{
(1..3).forEach {
println("emit:"+Thread.currentThread().name)
emit(it)
delay(100)
}
}
val coroutineDispatcher=Executors.newFixedThreadPool(3).asCoroutineDispatcher()
GlobalScope.launch(coroutineDispatcher) {
intFlow.flowOn(Dispatchers.IO).collect {
println("collet:"+Thread.currentThread().name)
println(it)
}
}
Thread.sleep(300000)
}
复制代码
看下执行结果:
这里我们要注意的是:
flowOn 指定的是 我们 生产端 的执行线程 。从代码中我们可以看出来,我们制定了launch的所属线程为 自定义的dispatcher,所以在 执行结果可以明显看出来 emit和collet的 所属线程是不一样的
如果你看过我之前的文章你就会知道 dispatchers.io和dispatchers.default 的所属线程是一样的,所以这里为了演示上的方便 我自定义了一个dispatcher。
这里熟悉rxjava的同学 可能就会很熟悉了 是很像rxjava的 subsscribeon和observeron的
我们可以再改变一下代码:
看下执行结果:
可以看出来 当你collect2次的时候 emit 也执行了两次
总结起来就是: 在一个Flow创建出来以后,不消费则不生产,多次消费 则多次生产。
所谓冷流,就是消费时才会产生的数据流,和我们的热流是相反的,热流的发送端并不依赖接收端
异常处理
发送端 如果发生了异常会咋样?
看下执行结果:
可以看出来 发送端抛出的异常,会在接收端 暴露出来,这对于接收端是有风险的。
再看下执行结果:
可以看出来 这个onComplete只能用于发现有没有异常,但他没有处理异常的能力 从输出中可以明显看到 程序发生了crash
她最大的作用是监听flow 是否结束
换一种写法:
增加了一个catch
再看结果
已经无大碍
另外要着重提一下的是,flow 没有提供取消操作 因为并不需要(可以仔细想想是为啥),如果一定要这么做,那么cancel一下 flow所属的协程即可
flow的另外几种写法
我们有时候 希望于 生产端和消费端的代码可以在一起 这样看起来处理方便。
可以采用如下的方法:
fun createFlow()=flow<Int>{
(1..3).forEach {
emit(it)
delay(100)
}
}.onEach {
println(it)
}
fun main()= runBlocking{
GlobalScope.launch() {
createFlow().collect()
}
Thread.sleep(300000)
}
复制代码
有时候我们在生产数据的时候 可能某些时候 希望切换下协程的context
背压
这个概念看上去高大上,其实也没啥难的,无非就是你要考虑到 当生产者的速度 大大超过 消费者的时候怎么处理?
对于客户端来说 其实这种场景不会特别多, 我举个例子
聊天软件大家都用过对吧,一般背后肯定是有个长连接的,用于接收消息,接收到消息以后 我们一般都会告知 对应的ui界面 去更新界面
考虑一种场景,当一个活跃用户 时隔半年 以后打开这个app,一下子几千条消息涌入进来,如果你还是之前的 收一条消息就更新一条ui 那明显 界面会卡死的。 这里就是一个背压的典型应用场景。
可以用代码看一下:
fun main()= runBlocking{
flow {
List(100){
emit(it)
}
}.conflate().collect {
println("collecting $it")
delay(100)
println("$it collected")
}
Thread.sleep(300000)
}
复制代码
conflate 典型的用新数据 覆盖老数据 来解决背压的问题
此外还有一个 collectLatest 她和conflate 的区别是,只有当新数据到来的时候老数据还没有处理完,才会用新数据去覆盖老数据