Kotlin协程Flow及管道中的buffer和bufferCapacity

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 条消息。

真正的积压可能来自:

  1. 原始 mChannel 自己的容量;
  2. Flow.buffer(10) 的容量;
  3. 下游正在处理但还未完成的元素;
  4. 其他业务层缓存。

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 里面做的是:

  1. 日志打印;
  2. 埋点;
  3. 状态修改;
  4. 数据库写入;
  5. 网络请求;
  6. 业务副作用;

那要非常小心。

因为它可能代表的是:

数据已经从 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}")

}

可能看到相同线程,也可能看到不同线程,取决于:

  1. 当前协程的 Dispatcher;
  2. 是否使用了 flowOn;
  3. 是否在 Dispatchers.Default、Dispatchers.IO 等线程池上;
  4. 调度器当时如何调度。

所以:

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) 只能短暂缓冲。

最终还是会:

  1. 上游挂起;
  2. Channel 积压;
  3. 内存上涨;
  4. 延迟越来越高。

这时候需要考虑:

  1. 增加消费者并发;
  2. 限流;
  3. 丢弃策略;
  4. 合并请求;
  5. 使用背压;
  6. 控制 Channel 容量;
  7. 监控队列长度。

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)

这可能提高短时间吞吐,但也可能带来问题:

  1. 内存占用增加;
  2. 下游延迟变大;
  3. 取消时未处理数据更多;
  4. 问题被隐藏,生产消费不平衡更晚暴露;
  5. 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 个元素。

实际开发中特别注意:

  1. buffer(10) 不是 mChannel 的容量;
  2. 它不是系统总容量限制;
  3. 它不是批处理大小;
  4. 默认缓冲区满了会挂起上游,不会丢数据;
  5. buffer 会让上下游并发执行;
  6. buffer 的位置非常重要;
  7. 放在 buffer 前面的 onEach 可能提前执行;
  8. 副作用、状态更新、消息确认等逻辑不要随便放在 buffer 前;
  9. 如果 mChannel 是无限容量,buffer(10) 不能防止内存积压;
  10. 多个 collector 收集 receiveAsFlow() 会竞争消费,不是广播;
  11. 如果需要广播,应考虑 SharedFlow;
  12. 如果需要整体容量控制,应优先控制 Channel 的容量,而不是只依赖 Flow 的 buffer。

一句话概括:

buffer(10) 控制的是 Flow 管道中某一段的预取和缓冲能力,它可以提升吞吐、解耦上下游,但不能替代 Channel 容量控制,也不能忽略副作用执行顺序。

相关推荐
传奇开心果编程1 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程2 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程3 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程3 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓3 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程3 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer
supabc1234 小时前
Celium:连接 Windows、Mac、Linux 与 Android,让远程访问和设备管理更简单
android·linux·windows·macos·远程访问·网络管理·celium
Dovis(誓平步青云)4 小时前
导览音频切换太快,旧讲解不能覆盖新展品
android·前端·javascript·ecmascript·音视频·宠物
熊猫钓鱼>_>20 小时前
Kotlin Multiplatform for OpenHarmony 实战:为 Reaktive 实现响应式原语适配(完整版 · 含摘要目录与技术图表)
华为·kotlin·大模型·ai编程·harmonyos·适配·reaktive
五彩小白20 小时前
自回归和上下文
android