心智模型:容量=“桶”,策略=“满了怎么办”
- 容量(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)
| 策略 | 满时行为 | send | trySend | 适用 |
|---|---|---|---|---|
| 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 + SUSPEND 或 RENDEZVOUS | 形成真实背压,保证可靠。 |
| 只关心最新(UI 状态/传感器/行情) | CONFLATED 或 capacity=32/64 + DROP_OLDEST | CONFLATED 保 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() }
易错与规约
- 把 UNLIMITED 当保险:只是把压力藏进内存,迟早爆。
- 不理解 DROP_ 与 trySend*:DROP_* 配置下 trySend 基本都会“成功”,只是按策略丢弃了某些值——注意业务可接受性。
- 容量过大:平均延迟升高(排队时间长),实时显示/交互场景会“滞后”。
- close 后继续 send:会抛 ClosedSendChannelException(trySend 返回 closed)。发送端可判 isClosedForSend 或用 runCatching 包装。
- 资源未释放:丢弃/取消路径必须配 onUndeliveredElement。
- 多消费者公平性:Channel 不保证公平;需要均衡请为每个消费者单独分配或自建调度。
一句话总结
容量决定排队深度,策略决定“满时取舍”,背压决定是否顶住上游。
必达选 SUSPEND(或 RENDEZVOUS);最新优先选 CONFLATED / DROP_OLDEST;谨慎对待 UNLIMITED。容量从 64 起步,监控队列压力再调优,并为未送达元素配置 onUndeliveredElement 保证资源安全。