本文已参与「新人创作礼」活动,一起开启掘金创作之路。
一、背压
背压(Back pressure)来自物理中的概念,指的是流体受到的与流动方向相反的压力,又称为反压。
举个通俗的例子,当有人在背后推你时,你的背上会感到压力,同时,由牛顿第三定律可知,你的背也会给推你的人一个压力。由你的背施加的这个压力就称为背压。
牛顿第三定律:当两个物体相互作用时,彼此施加于对方的力,其大小相等、方向相反。 力必会成双结对地出现:其中一道力称为「作用力」;而另一道力称为「反作用力」。
再举个高端一点的例子,在选车时,不少人都会关注汽车的推背感,也就是汽车加速时,座椅靠背施加给驾驶员和乘客后背的推力。推背感强,说明汽车的加速性能好。在座椅给后背施加压力的同时,背也会给座椅施加一个反作用力,这个反作用力就是背压。
那么 Flow 当中的背压是什么意思呢?
设想你有一节水管,水流从一端流入,从另一端流出,非常顺利。但如果此时将流出的一头变窄,那么变窄的瞬间,入口处的水流就会受到来自出口处的水流产生的背压。
反映到 Flow 中,就是当下游的消费者的速率小于上游生产者的速率时,生产者就会收到来自消费者的背压。
看一段示例代码:
runBlocking {
val time = measureTimeMillis {
flow {
(1..5).forEach {
delay(200)
println("emit: $it, ${System.currentTimeMillis()}, ${Thread.currentThread().name}")
emit(it)
}
}.collect {
// 消费效率较低
delay(500)
println("Collect $it, ${System.currentTimeMillis()}, ${Thread.currentThread().name}")
}
}
println("time: $time")
}
在这段代码中,上游每隔 200ms 生产一个值,下游每隔 500ms 消费一个值。这就是一个典型的生产速率大于消费速率的场景。运行程序,输出如下:
emit: 1, 1650010994242, Test worker @coroutine#1
Collect 1, 1650010994761, Test worker @coroutine#1
emit: 2, 1650010994965, Test worker @coroutine#1
Collect 2, 1650010995469, Test worker @coroutine#1
emit: 3, 1650010995674, Test worker @coroutine#1
Collect 3, 1650010996178, Test worker @coroutine#1
emit: 4, 1650010996383, Test worker @coroutine#1
Collect 4, 1650010996899, Test worker @coroutine#1
emit: 5, 1650010997103, Test worker @coroutine#1
Collect 5, 1650010997607, Test worker @coroutine#1
time: 3585
大致花了 5 * 700 = 3500ms。
二、解决背压方式一:buffer()
解决背压最常见的方式是通过 buffer() 开启缓存:
runBlocking {
val time = measureTimeMillis {
flow {
(1..5).forEach {
delay(200)
println("emit: $it, ${System.currentTimeMillis()}, ${Thread.currentThread().name}")
emit(it)
}
}.buffer().collect {
// 消费效率较低
delay(500)
println("Collect $it, ${System.currentTimeMillis()}, ${Thread.currentThread().name}")
}
}
println("time: $time")
}
buffer() 函数可以传入一个 capacity 参数,表示缓冲区的容量,不传表示使用默认值 64。运行程序,输出如下:
emit: 1, 1650010930344, Test worker @coroutine#2
emit: 2, 1650010930561, Test worker @coroutine#2
emit: 3, 1650010930763, Test worker @coroutine#2
Collect 1, 1650010930857, Test worker @coroutine#1
emit: 4, 1650010930966, Test worker @coroutine#2
emit: 5, 1650010931172, Test worker @coroutine#2
Collect 2, 1650010931358, Test worker @coroutine#1
Collect 3, 1650010931875, Test worker @coroutine#1
Collect 4, 1650010932384, Test worker @coroutine#1
Collect 5, 1650010932885, Test worker @coroutine#1
time: 2772
耗时约 5 * 500 + 200 = 2700ms。可以看出程序运行效率提高了。
但笔者认为此例中效率的提高实际上跟缓冲没太大关系。只是使用 buffer 时,数据的生成和收集变成了异步的,从打印的线程名可以看出这一点。
我们也可以通过 flowOn 将协程的创建和收集指定到不同的协程执行,提高其运行效率:
runBlocking {
val time = measureTimeMillis {
flow {
(1..5).forEach {
delay(200)
println("emit: $it, ${System.currentTimeMillis()}, ${Thread.currentThread().name}")
emit(it)
}
}.flowOn(Dispatchers.Default).collect {
// 消费效率较低
delay(500)
println("Collect $it, ${System.currentTimeMillis()}, ${Thread.currentThread().name}")
}
}
println("time: $time")
}
运行程序,输出如下:
emit: 1, 1650011026339, DefaultDispatcher-worker-1 @coroutine#2
emit: 2, 1650011026558, DefaultDispatcher-worker-1 @coroutine#2
emit: 3, 1650011026761, DefaultDispatcher-worker-1 @coroutine#2
Collect 1, 1650011026852, Test worker @coroutine#1
emit: 4, 1650011026962, DefaultDispatcher-worker-1 @coroutine#2
emit: 5, 1650011027164, DefaultDispatcher-worker-1 @coroutine#2
Collect 2, 1650011027364, Test worker @coroutine#1
Collect 3, 1650011027878, Test worker @coroutine#1
Collect 4, 1650011028394, Test worker @coroutine#1
Collect 5, 1650011028896, Test worker @coroutine#1
time: 2788
可以看出,这样也只会花费约 2700ms。
buffer() 的实际意义是当产生背压时,将生产者生产的数据先暂时缓冲起来,慢慢发给消费者。
三、解决背压方式二:conflate()
conflate 译为合并,conflate() 函数用于合并上游数据。当下游消费不过来时,conflate() 函数只会保留最新一个生产的元素,直到消费者重新开始消费,conflate() 函数再把当前最新的元素发给消费者。
使用 conflate() 函数解决背压:
runBlocking {
val time = measureTimeMillis {
flow {
(1..5).forEach {
delay(200)
println("emit: $it, ${System.currentTimeMillis()}, ${Thread.currentThread().name}")
emit(it)
}
}.conflate().collect {
// 消费效率较低
println("Collect $it, ${System.currentTimeMillis()}, ${Thread.currentThread().name}")
delay(500)
println("Collect $it done, ${System.currentTimeMillis()}, ${Thread.currentThread().name}")
}
}
println("time: $time")
}
运行程序,输出如下:
emit: 1, 1650381439327, Test worker @coroutine#2
Collect 1, 1650381439335, Test worker @coroutine#1
emit: 2, 1650381439537, Test worker @coroutine#2
emit: 3, 1650381439741, Test worker @coroutine#2
Collect 1 done, 1650381439837, Test worker @coroutine#1
Collect 3, 1650381439838, Test worker @coroutine#1
emit: 4, 1650381439946, Test worker @coroutine#2
emit: 5, 1650381440149, Test worker @coroutine#2
Collect 3 done, 1650381440339, Test worker @coroutine#1
Collect 5, 1650381440340, Test worker @coroutine#1
Collect 5 done, 1650381440840, Test worker @coroutine#1
time: 1761
可以看出,当 collect() 消费不过来时,conflate() 只保留了最后一个生产的元素。一直到 collect() 将上一个数据消费完后,conflate() 函数再把当前最新生产的元素发给消费者。
所以这里只处理了 1、3、5 三个数,总时间约为 3 * 500 + 200 = 1700ms。
四、解决背压方式三:collectLatest()
collectLatest() 函数表示处理最新值,它和 conflate() 是有很大区别的。conflate() 函数不会打断消费者的消费过程,而 collectLatest() 会在最新值来的时候,将当前消费者的消费过程取消,然后令消费者处理最新值。
runBlocking {
val time = measureTimeMillis {
flow {
(1..5).forEach {
delay(200)
println("emit: $it, ${System.currentTimeMillis()}, ${Thread.currentThread().name}")
emit(it)
}
}.collectLatest {
// 消费效率较低
println("Collect $it, ${System.currentTimeMillis()}, ${Thread.currentThread().name}")
delay(500)
println("Collect $it done, ${System.currentTimeMillis()}, ${Thread.currentThread().name}")
}
}
println("time: $time")
}
运行程序,输出如下:
emit: 1, 1650381977669, Test worker @coroutine#2
Collect 1, 1650381977677, Test worker @coroutine#3
emit: 2, 1650381977879, Test worker @coroutine#2
Collect 2, 1650381977917, Test worker @coroutine#4
emit: 3, 1650381978121, Test worker @coroutine#2
Collect 3, 1650381978123, Test worker @coroutine#5
emit: 4, 1650381978325, Test worker @coroutine#2
Collect 4, 1650381978327, Test worker @coroutine#6
emit: 5, 1650381978528, Test worker @coroutine#2
Collect 5, 1650381978529, Test worker @coroutine#7
Collect 5 done, 1650381979030, Test worker @coroutine#7
time: 1599
可以看出,只有最后一个值被消费完成了。如果在消费者的代码块中 catch 异常,可以捕获到 ChildCancelledException。
花费总时间约为 5 * 200 + 500 = 1500ms。
五、小结
本文我们介绍了在 Flow 中对背压的处理方式。常见的处理方式有 buffer()、conflate()、collectLatest() 三个函数,这三种方式各有区别,需要根据应用场景选择合适的处理方式。