详解channel容量、背压与溢出策略

151 阅读4分钟

心智模型:容量=“桶”,策略=“满了怎么办”

  • 容量(capacity)决定队列能“暂存”多少元素;
  • 背压send 是否会挂起 决定;
  • 溢出策略决定“满桶时挂起还是丢弃哪一个(最老/最新)”;
  • CONFLATED 是“只有最新”的特种通道,相当于桶位=1 且老值会被覆盖。

常见容量档位(以及内部语义)

容量语义 / 使用时机背压备注
RENDEZVOUS (0)无缓冲,“一发一收握手”。每条消息即时交接。强背压:send 一定等 receive最稳不丢,但吞吐受限,适合“每条必达且速率接近”的点对点。
BUFFERED(默认≈64)小缓冲,削峰填谷。满了再背压(看策略)通用默认,常配 SUSPEND。
UNLIMITED无限链表无背压(直到 OOM)谨慎!仅在低频/开销小且可控时用。
CONFLATED只保最新(覆盖旧值)。基本无背压适合 UI/传感器这类“只关心最新状态”。

说明:默认构造 Channel() 等价于 Channel(BUFFERED),容量实现通常≈64(版本可能略有变化,以官方为准)。


溢出策略(满桶时的三选一)

Channel(capacity = N, onBufferOverflow = BufferOverflow.XXX)
策略满时行为sendtrySend适用
SUSPEND(默认)不丢,等待空位挂起直到有空间失败(返回 closed/failed)每条必达,形成真正背压
DROP_OLDEST丢最老,再放新值不挂起(直接替换最老)成功(旧值被丢)滑窗”语义:保近期、高吞吐
DROP_LATEST丢本次,保已有不挂起(这次被丢)失败(或返回 dropped)保护已积压数据,拒新

CONFLATED 的区别:

  • CONFLATED 实质是“只保一个最新”,接收侧永远读到最近一次;

  • DROP_OLDEST 适用于 容量>1 的滑窗(比如 32/64),保留最近的一串;

  • 若你只要“最后一个”,CONFLATED 更直观与省内存。


send / trySend 与策略的联动(最易踩的点)

  • send(挂起式)

    • SUSPEND:满则挂起(形成背压)。
    • DROP_OLDEST / DROP_LATEST:不会挂起;会按策略丢弃并继续。
  • trySend(非挂起式)

    • SUSPEND:满则失败(isSuccess=false)。

    • DROP_*:即便满也成功(内部已按策略丢弃别人/丢自己)。

    • CONFLATED:基本成功(直接覆盖旧值),除非已关闭。

常用“先 try,失败再挂起”写法(既优先高吞吐,又保底不丢):

suspend fun <E> SendChannel<E>.offerOrSend(e: E) {
  if (!trySend(e).isSuccess) send(e)  // SUSPEND 下满才会走到 send 挂起
}

选型矩阵(按业务语义选“容量 × 策略”)

业务诉求推荐配置说明
每条必达,宁可顶住上游capacity = 32/64/128 + SUSPENDRENDEZVOUS形成真实背压,保证可靠。
只关心最新(UI 状态/传感器/行情)CONFLATED 或 capacity=32/64 + DROP_OLDESTCONFLATED 保 1 个最新;滑窗能保最近一小段。
滑窗高吞吐(保近丢远)capacity=32/64/128 + DROP_OLDEST防爆内存,降低尾延迟。
保护已入队(拒绝新流量)capacity=N + DROP_LATEST场景少见:必须顺序处理旧数据。
低频控制台/日志BUFFERED + SUSPEND 或 DROP_OLDEST低频用 SUSPEND;高频日志建议 DROP_OLDEST 防阻塞。
绝不建议UNLIMITED隐性堆积 → OOM 风险极高。

容量经验值:32/64/128 常见足够;容量过大=高延迟(排队久),过小=抖动。优先从 64 起步,结合监控调。


监控与资源安全

  • 监控队列压力:Channel 无标准 size,可在生产/消费点自增自减一个 AtomicInteger 记录长度,打点 p95/p99。
  • onUndeliveredElement:当元素因为取消/关闭/丢弃未能送达接收端时回调,务必在持资源类型(文件、Bitmap、ByteBuffer)上释放:
val ch = Channel<ByteBuffer>(
  capacity = 64,
  onBufferOverflow = BufferOverflow.DROP_OLDEST,
  onUndeliveredElement = { buf -> try { buf.clear() } catch (_: Throwable) {} }
)
  • 关闭语义

    • close() :不再接收新元素,但会让接收侧把已入队的读完
    • cancel() :立即结束,可能丢弃未读元素(激进停机)。

可抄用模板

1) UI 只要最新(不阻塞生产者)

val latest = Channel<State>(capacity = Channel.CONFLATED)

producer.launch(Dispatchers.Default) {
  while (isActive) {
    latest.trySend(snapshot()) // 覆盖旧值,永不挂起
  }
}

consumer.launch(Dispatchers.Main) {
  for (s in latest) render(s)
}

2) 滑窗削峰(高吞吐 + 保近)

val ticks = Channel<Tick>(capacity = 64, onBufferOverflow = BufferOverflow.DROP_OLDEST)

producer.launch(Dispatchers.IO) {
  while (isActive) ticks.trySend(readTick())
}

consumer.launch(Dispatchers.Default) {
  for (t in ticks) process(t)
}

3) 必达流水线(多阶段 + 真背压)

val c12 = Channel<Task>(64, BufferOverflow.SUSPEND)
val c23 = Channel<Result>(32, BufferOverflow.SUSPEND)

// stage1
launch(Dispatchers.IO) { source().forEach { c12.send(it) }; c12.close() }

// stage2(并行)
repeat(4) {
  launch(Dispatchers.Default) { for (t in c12) c23.send(handle(t)) }
}

// stage3
launch(Dispatchers.IO) { for (r in c23) sink(r); c23.close() }

易错与规约

  1. 把 UNLIMITED 当保险:只是把压力藏进内存,迟早爆。
  2. 不理解 DROP_ 与 trySend*:DROP_* 配置下 trySend 基本都会“成功”,只是按策略丢弃了某些值——注意业务可接受性
  3. 容量过大:平均延迟升高(排队时间长),实时显示/交互场景会“滞后”。
  4. close 后继续 send:会抛 ClosedSendChannelException(trySend 返回 closed)。发送端可判 isClosedForSend 或用 runCatching 包装。
  5. 资源未释放:丢弃/取消路径必须配 onUndeliveredElement。
  6. 多消费者公平性:Channel 不保证公平;需要均衡请为每个消费者单独分配或自建调度。

一句话总结

容量决定排队深度,策略决定“满时取舍”,背压决定是否顶住上游。

必达选 SUSPEND(或 RENDEZVOUS);最新优先选 CONFLATED / DROP_OLDEST;谨慎对待 UNLIMITED。容量从 64 起步,监控队列压力再调优,并为未送达元素配置 onUndeliveredElement 保证资源安全。