Kotlin协程Flow及管道中的buffer和bufferCapacity
mChannel.receiveAsFlow()
.onEach {
// do something
}
.buffer(bufferCapacity)
其中:
bufferCapacity = 10
这里的 bufferCapacity = 10 表示的是:
在 Flow 管道中,onEach 上游和下游之间增加一个容量为 10 的缓冲区。
它不是 mChannel 本身的容量,也不是整个系统最多只能缓存 10 个元素。
1. 先理解 Channel 和 receiveAsFlow()
假设有一个 Channel:
val mChannel = Channel<Int>()
然后:
mChannel.receiveAsFlow()
表示把这个 Channel 转成一个 Flow。也就是说,后面可以用 Flow 的操作符来处理 Channel 中收到的数据:
mChannel.receiveAsFlow()
.onEach {
println("receive: $it")
}
.collect {
println("collect: $it")
}
需要注意:
receiveAsFlow()
本质上还是在从 mChannel 中接收数据。
它不是把 Channel 复制了一份,也不是广播。
如果多个地方同时 collect 这个 Flow,那么多个 collector 会竞争消费同一个 Channel 中的数据。
2. buffer(10) 的核心意义
mChannel.receiveAsFlow()
.onEach {
// do something
}
.buffer(10)
可以理解为:
mChannel
-> receiveAsFlow()
-> onEach { ... }
-> 容量为 10 的 Flow 内部缓冲区
-> 下游操作符 / collect
也就是说,buffer(10) 会在 Flow 管道中创建一个内部缓冲区,最多可以暂存 10 个元素。
3. 它不是限制 mChannel 的容量
这是第一个重点。
.buffer(10)
限制的是 Flow 内部某一段管道之间的缓冲容量。
它并不会把原来的 mChannel 变成容量为 10 的 Channel。
例如:
val mChannel = Channel<Int>(Channel.UNLIMITED)
mChannel.receiveAsFlow()
.onEach { println(it) }
.buffer(10)
.collect { delay(1000) }
这里虽然 Flow 上写了:
.buffer(10)
但如果 mChannel 本身是 Channel.UNLIMITED,生产者还是可能不断往 mChannel 里发送数据,最终导致内存压力。
所以:
buffer(10) 不等于系统最多积压 10 条消息。
真正的积压可能来自:
- 原始 mChannel 自己的容量;
- Flow.buffer(10) 的容量;
- 下游正在处理但还未完成的元素;
- 其他业务层缓存。
4. buffer(10) 会让上下游并发执行
没有 buffer 的时候,Flow 通常是顺序执行的。
例如:
mChannel.receiveAsFlow()
.onEach {
println("onEach: $it")
}
.collect {
delay(1000)
println("collect: $it")
}
大致执行方式是:
收到 1
执行 onEach 1
执行 collect 1,耗时 1 秒
收到 2
执行 onEach 2
执行 collect 2,耗时 1 秒
收到 3
执行 onEach 3
执行 collect 3,耗时 1 秒
上游和下游基本是串行的。
但是加了:
.buffer(10)
之后:
mChannel.receiveAsFlow()
.onEach {
println("onEach: $it")
}
.buffer(10)
.collect {
delay(1000)
println("collect: $it")
}
它更像是:
val internalBuffer = Channel<Int>(capacity = 10)
launch {
for (item in mChannel) {
println("onEach: $item")
internalBuffer.send(item)
}
}
launch {
for (item in internalBuffer) {
delay(1000)
println("collect: $item")
}
}
也就是说,buffer(10) 会让它前面的部分和后面的部分可以并发运行。
5. buffer(10) 的实际效果
假设下游处理很慢:
mChannel.receiveAsFlow()
.onEach {
println("onEach: $it")
}
.buffer(10)
.collect {
delay(1000)
println("collect: $it")
}
如果 mChannel 中很快来了很多数据:
1, 2, 3, 4, 5, ... 100
那么上游可能会先快速执行:
onEach: 1
onEach: 2
onEach: 3
...
onEach: 10
这些元素会进入 buffer(10) 的内部缓冲区。
下游 collect 则慢慢处理:
collect: 1
1 秒后
collect: 2
1 秒后
collect: 3
当内部缓冲区满了,比如已经缓存了 10 个元素,而下游还没消费掉,那么上游就会被挂起。
所以默认情况下:
.buffer(10)
的行为是:
最多缓存 10 个元素;满了之后,上游挂起等待下游消费。
6. 默认溢出策略是挂起,不是丢弃
buffer 的完整形式大概是:
.buffer(
capacity = 10,
onBufferOverflow = BufferOverflow.SUSPEND
)
也就是说,默认是:
BufferOverflow.SUSPEND
当缓冲区满了,上游会暂停,而不是丢弃数据。
例如:
.buffer(10)
等价理解为:
Kotlin
.buffer(
capacity = 10,
onBufferOverflow = BufferOverflow.SUSPEND
)
所以如果没有显式指定丢弃策略,数据不会因为 buffer 满了就自动丢掉,而是上游会被反压。
7. bufferCapacity = 10 不是 batch size
这个也很容易误解。
.buffer(10)
不是说每 10 个元素处理一次。
它不是批处理。
它只是说:
上游最多可以比下游提前生产 10 个元素。
如果想每 10 个元素组成一批处理,那需要自己做批量逻辑,比如:
chunked
或者手写收集逻辑,而不是用 buffer(10)。
8. 这个位置的 buffer(10) 特别关键
代码是:
mChannel.receiveAsFlow()
.onEach {
// do something
}
.buffer(10)
注意,buffer(10) 在 onEach 后面。
这意味着:
onEach { ... }
属于 buffer 的上游。
也就是说,onEach 里面的代码可能会先执行,并且最多领先下游大约 10 个元素。
例如:
mChannel.receiveAsFlow()
.onEach {
println("onEach: $it")
}
.buffer(10)
.collect {
delay(1000)
println("collect: $it")
}
可能出现:
onEach: 1
onEach: 2
onEach: 3
onEach: 4
onEach: 5
onEach: 6
onEach: 7
onEach: 8
onEach: 9
onEach: 10
collect: 1
collect: 2
...
所以如果在 onEach 里面做的是:
- 日志打印;
- 埋点;
- 状态修改;
- 数据库写入;
- 网络请求;
- 业务副作用;
那要非常小心。
因为它可能代表的是:
数据已经从 Channel 中取出来,并且进入 Flow buffer 了。
但并不代表:
下游已经真正处理完成了。
9. 如果副作用必须和最终消费绑定,别放在 buffer 前面
例如这样写:
mChannel.receiveAsFlow()
.onEach {
markAsProcessing(it)
}
.buffer(10)
.collect {
process(it)
}
这里 markAsProcessing(it) 可能比 process(it) 提前执行很多。
如果程序在中间取消了,可能出现:
已经 markAsProcessing
但还没有真正 process
这在任务队列、订单处理、消息确认场景中很危险。
更安全的写法可能是:
mChannel.receiveAsFlow()
.buffer(10)
.collect {
markAsProcessing(it)
process(it)
}
或者:
mChannel.receiveAsFlow()
.buffer(10)
.onEach {
markAsProcessing(it)
}
.collect {
process(it)
}
但具体放在哪里,要看具体业务。
重点是:
buffer 前面的逻辑表示"进入缓冲区之前执行",buffer 后面的逻辑表示"从缓冲区取出之后执行"。
10. buffer(10) 会影响线程吗?
buffer 会引入协程边界,让上下游可以在不同协程中执行。
但它不一定意味着一定切换到不同线程。
例如:
mChannel.receiveAsFlow()
.onEach {
println("onEach thread = ${Thread.currentThread().name}")
}
.buffer(10)
.collect {
println("collect thread = ${Thread.currentThread().name}")
}
可能看到相同线程,也可能看到不同线程,取决于:
- 当前协程的 Dispatcher;
- 是否使用了 flowOn;
- 是否在 Dispatchers.Default、Dispatchers.IO 等线程池上;
- 调度器当时如何调度。
所以:
buffer(10)
主要改变的是协程执行结构和背压关系,不是直接指定线程。
如果想明确改变上游执行线程,需要用:
.flowOn(Dispatchers.IO)
例如:
mChannel.receiveAsFlow()
.onEach {
println("onEach: ${Thread.currentThread().name}")
}
.flowOn(Dispatchers.IO)
.buffer(10)
.collect {
println("collect: ${Thread.currentThread().name}")
}
不过 flowOn 和 buffer 的位置也会影响执行边界,实际项目中要谨慎设计。
11. 和 Channel 自身容量的关系
这是实际开发中最重要的点之一。
假设:
val mChannel = Channel<Int>(capacity = 100)
然后:
mChannel.receiveAsFlow()
.onEach { }
.buffer(10)
.collect { }
那么系统中可能积压的数据并不是 10 个,而是:
mChannel 自身最多 100 个
Flow buffer 最多 10 个
正在被处理的元素
所以总积压可能超过 110。
如果:
val mChannel = Channel<Int>(Channel.UNLIMITED)
那么即使 Flow buffer 是 10,也无法防止 mChannel 本身无限积压。
所以如果想控制整体内存,一定要控制源头:
val mChannel = Channel<Int>(capacity = 100)
而不是只依赖:
.buffer(10)
12. 不同 Channel 容量下的效果
12.1 Rendezvous Channel
val mChannel = Channel<Int>()
默认是容量为 0 的 Rendezvous Channel。
发送方 send 和接收方 receive 要配对。
但是如果后面加了:
mChannel.receiveAsFlow()
.buffer(10)
.collect { delay(1000) }
那么 Flow 会从 mChannel 中提前接收一些数据放入 buffer。
这会让发送方看起来可以连续发送大约 10 个元素,而不必每个都等下游处理完成。
12.2 Bounded Channel
val mChannel = Channel<Int>(capacity = 100)
再加:
.buffer(10)
意味着整体缓冲能力更强。
Channel 里面可以积压 100 个
Flow buffer 里面可以积压 10 个
这种情况吞吐可能更好,但延迟和内存占用也可能增加。
12.3 Unlimited Channel
val mChannel = Channel<Int>(Channel.UNLIMITED)
这种情况下:
.buffer(10)
对防止内存无限增长帮助不大。
因为生产者可以先把大量数据塞进 mChannel。
如果生产速度长期大于消费速度,仍然可能导致内存问题。
13. 实际开发中的注意事项
13.1 不要把 buffer(10) 当成整体容量限制
错误理解:
mChannel.receiveAsFlow()
.buffer(10)
以为最多积压 10 个任务。
正确理解:
只是 Flow 管道中某一段最多缓冲 10 个元素。
如果 mChannel 本身有容量,或者是无限容量,总积压可能远超过 10。
13.2 注意 buffer 的位置
下面两段代码语义不同。
第一种:
mChannel.receiveAsFlow()
.onEach {
println("onEach before buffer: $it")
}
.buffer(10)
.collect {
delay(1000)
println("collect: $it")
}
onEach 在 buffer 前面,可能提前执行。
第二种:
Kotlin
mChannel.receiveAsFlow()
.buffer(10)
.onEach {
println("onEach after buffer: $it")
}
.collect {
delay(1000)
println("collect: $it")
}
onEach 在 buffer 后面,只有下游从 buffer 中取出元素后才执行。
所以:
buffer 的位置决定了哪些逻辑可以提前执行,哪些逻辑必须跟随下游消费节奏。
13.3 小心副作用提前执行
例如:
Kotlin
mChannel.receiveAsFlow()
.onEach {
updateDbStatus(it.id, "PROCESSING")
}
.buffer(10)
.collect {
reallyProcess(it)
}
如果 reallyProcess 很慢,updateDbStatus 可能提前对 10 个任务执行。
如果中途协程取消,可能导致状态不一致。
任务处理类业务中,建议把关键副作用放在真正处理逻辑附近。
13.4 如果下游很慢,buffer 只能缓解,不能解决根因
例如:
mChannel.receiveAsFlow()
.buffer(10)
.collect {
delay(5000)
process(it)
}
如果生产者每秒发送 1000 条,而消费者 5 秒处理 1 条,那么 buffer(10) 只能短暂缓冲。
最终还是会:
- 上游挂起;
- Channel 积压;
- 内存上涨;
- 延迟越来越高。
这时候需要考虑:
- 增加消费者并发;
- 限流;
- 丢弃策略;
- 合并请求;
- 使用背压;
- 控制 Channel 容量;
- 监控队列长度。
13.5 默认不会丢数据,但可能导致上游挂起
.buffer(10)
默认是挂起策略:
BufferOverflow.SUSPEND
如果不希望上游挂起,而是希望丢弃,可以显式写:
.buffer(
capacity = 10,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
或者:
.buffer(
capacity = 10,
onBufferOverflow = BufferOverflow.DROP_LATEST
)
例如 UI 场景中,只关心最新状态,可以考虑丢弃旧值。
但是任务处理、订单处理、支付消息这类场景一般不能随便丢。
13.6 不要随便用很大的 buffer
例如:
.buffer(10_000)
这可能提高短时间吞吐,但也可能带来问题:
- 内存占用增加;
- 下游延迟变大;
- 取消时未处理数据更多;
- 问题被隐藏,生产消费不平衡更晚暴露;
- OOM 风险增加。
buffer 不是越大越好。
合理做法是根据业务选择:
.buffer(10)
.buffer(64)
.buffer(100)
然后结合压测和监控调整。
13.7 receiveAsFlow() 不是广播
如果你需要广播,让多个订阅者都收到同一份数据,应该考虑:
SharedFlow
或者:
StateFlow
而不是 Channel.receiveAsFlow()。
13.8 注意取消和关闭
Channel 转成 Flow 后,仍然要关注生命周期。
比如:
mChannel.close()
正常关闭后,receiveAsFlow() 对应的 Flow 会结束。
如果 Channel 是异常关闭:
mChannel.close(cause)
那么 Flow 收集时也可能收到异常。
实际开发中要注意:
try {
mChannel.receiveAsFlow()
.buffer(10)
.collect {
process(it)
}
} catch (e: Throwable) {
// handle error
}
同时,不要只取消 collector,却忘了生产者还在不断 send。
否则可能造成生产端挂起或者资源泄漏。
14. 一个例子:观察 buffer 的效果
val mChannel = Channel<Int>()
launch {
repeat(100) {
println("send: $it")
mChannel.send(it)
}
}
mChannel.receiveAsFlow()
.onEach {
println("onEach: it, thread={Thread.currentThread().name}")
}
.buffer(10)
.collect {
delay(1000)
println("collect: it, thread={Thread.currentThread().name}")
}
可能会看到 onEach 先快速打印多个元素,而 collect 慢慢打印。
原因就是:
.buffer(10)
允许上游比下游最多提前大约 10 个元素。
15. 如果希望限制整个系统最多 10 个任务,应该怎么做?
不要只写:
mChannel.receiveAsFlow()
.buffer(10)
更应该从源头控制:
val mChannel = Channel<Task>(capacity = 10)
然后根据需要决定 Flow 里面是否还要 buffer:
mChannel.receiveAsFlow()
.collect {
process(it)
}
如果已经在 Channel 层做了容量限制,Flow 里是否还需要 buffer 要谨慎评估。
因为:
Channel<Task>(10)
加上:
Kotlin
.buffer(10)
整体上可能变成:
Channel 10 个
Flow buffer 10 个
正在处理的 1 个
总共可能有 21 个左右处于系统中。
16. 总结
对于:
mChannel.receiveAsFlow()
.onEach {
}
.buffer(10)
bufferCapacity = 10 的意义是:
在 Flow 的 onEach 和后续下游之间创建一个容量为 10 的内部缓冲区,让上游最多可以领先下游 10 个元素。
实际开发中特别注意:
- buffer(10) 不是 mChannel 的容量;
- 它不是系统总容量限制;
- 它不是批处理大小;
- 默认缓冲区满了会挂起上游,不会丢数据;
- buffer 会让上下游并发执行;
- buffer 的位置非常重要;
- 放在 buffer 前面的 onEach 可能提前执行;
- 副作用、状态更新、消息确认等逻辑不要随便放在 buffer 前;
- 如果 mChannel 是无限容量,buffer(10) 不能防止内存积压;
- 多个 collector 收集 receiveAsFlow() 会竞争消费,不是广播;
- 如果需要广播,应考虑 SharedFlow;
- 如果需要整体容量控制,应优先控制 Channel 的容量,而不是只依赖 Flow 的 buffer。
一句话概括:
buffer(10) 控制的是 Flow 管道中某一段的预取和缓冲能力,它可以提升吞吐、解耦上下游,但不能替代 Channel 容量控制,也不能忽略副作用执行顺序。