1) Channel 是什么:一对一“邮筒”
- 点对点:Channel 天然适合 1 生产者 ↔ 1 消费者。也支持多生产/多消费,但不保证公平/顺序(不要依赖)。
- 冷 vs 热:Channel 是热消息队列(创建即存在),和 Flow(冷)互补:可用 receiveAsFlow()/consumeAsFlow() 互转。
2) 核心参数 = “容量 + 背压/丢弃策略”
val ch = Channel<E>(
capacity = N, // 0=RENDEZVOUS, >0=有界, UNLIMITED, CONFLATED, BUFFERED(≈默认合适值)
onBufferOverflow = BufferOverflow.SUSPEND // or DROP_OLDEST / DROP_LATEST
// onUndeliveredElement = { e -> ... } // 元素未能送达时的清理回调(释放资源等)
)
-
capacity(缓冲大小)
- RENDEZVOUS(0):无缓冲,send 必须等 receive 立刻配对(最强背压)。
- N > 0:固定有界环形缓冲(常用 16/64/256…)。
- UNLIMITED:链表无限长(除非 OOM,一般别用在高频)。
- CONFLATED:仅保“最新” (新值覆盖旧值;接收侧永远拿到最近一次)。
- BUFFERED:库给的合理默认(≈几十)。常够用。
-
onBufferOverflow(缓冲满时)
-
SUSPEND:默认。不丢,send 挂起,形成背压。
-
DROP_OLDEST:丢最老的,再放新值(滑窗语义,适合 UI“只关心最新”)。
-
DROP_LATEST:丢当前发送的新值(保老数据)。
-
直观选择:
-
每条必达:capacity=N + SUSPEND。
-
只要最新:CONFLATED 或 capacity=N + DROP_OLDEST。
-
吞吐优先但可小丢:capacity=N + DROP_OLDEST。
-
绝不建议:UNLIMITED(易隐性堆积 → OOM)。
3) send/receive/trySend/tryReceive:阻塞与非阻塞
suspend fun SendChannel<E>.send(e: E) // 可能挂起(按策略)
fun SendChannel<E>.trySend(e: E): ChannelResult<Unit> // 不挂起,失败立即返回
suspend fun ReceiveChannel<E>.receive(): E // 可能挂起,队列空则等
fun ReceiveChannel<E>.tryReceive(): ChannelResult<E> // 不挂起,空则失败
- 背压点在 send:当容量耗尽且策略 SUSPEND → send 挂起,生产者被“顶住”。
- 用 trySend 可 不挂起,配合 DROP_OLDEST 实现“满了就丢旧保新”。
4) 关停与异常
ch.close(cause?: Throwable?) // 关闭发送端;接收侧读完现存数据后结束
ch.cancel(cause) // 立刻取消,丢弃未读数据(更激进)
ch.isClosedForSend / ch.isClosedForReceive
- 发送端关闭:trySend 失败,send 抛 ClosedSendChannelException。
- 接收端完成:for (e in ch) 自然结束;或 receive() 抛 ClosedReceiveChannelException。
- onUndeliveredElement:元素因取消/关闭未达接收者时回调一次,及时释放资源(文件句柄、Bitmap 等)。
5) 典型范式(点对点 + 明确背压)
(A) 最纯粹点对点:RENDEZVOUS(0)
“一手交钱一手交货”,强背压,永不丢。
val ch = Channel<Task>(capacity = Channel.RENDEZVOUS)
val producer = launch {
for (i in 0 until 1000) {
ch.send(Task(i)) // 消费者没收,这里就挂起
}
ch.close()
}
val consumer = launch {
for (t in ch) handle(t) // 逐个处理
}
(B) 有界 + 必达:capacity=N, SUSPEND
允许短暂排队,满了就顶住上游。
val ch = Channel<Event>(capacity = 64, onBufferOverflow = BufferOverflow.SUSPEND)
(C) UI 只要最新:DROP_OLDEST/CONFLATED
// 方案1:滑窗
val ch = Channel<Tick>(capacity = 32, onBufferOverflow = BufferOverflow.DROP_OLDEST)
// 方案2:永远只保最新
val latest = Channel<Tick>(capacity = Channel.CONFLATED)
producer.launch {
while (isActive) {
val t = readSensor()
latest.trySend(t) // 不阻塞,自动覆盖旧值
}
}
consumer.launch {
for (t in latest) render(t) // 始终最新
}
(D) 生产-消费流水线(多阶段背压)
// stage1 -> stage2 -> stage3
val c12 = Channel<Job>(capacity = 64, onBufferOverflow = BufferOverflow.SUSPEND)
val c23 = Channel<Result>(capacity = 32, onBufferOverflow = BufferOverflow.SUSPEND)
launch(Dispatchers.IO) { // stage1
source().forEach { c12.send(it) }
c12.close()
}
repeat(4) { // stage2:并行处理
launch(Dispatchers.Default) {
for (job in c12) c23.send(process(job))
}
}
launch(Dispatchers.IO) { // stage3:落盘/上传
for (r in c23) sink(r)
c23.close()
}
每一段容量就是节流阀:c12=64 限制“在处理队列”长度,c23=32 限制输出堆积。
(E) 限速/并发上限(把 Channel 当信号量)
val permits = Channel<Unit>(capacity = 8).apply { repeat(8) { trySend(Unit) } }
suspend fun <T> withPermit(block: suspend () -> T): T {
permits.receive() // 拿令牌(若无则挂起)
return try { block() } finally { permits.trySend(Unit) } // 还令牌
}
6) 与 Flow 的互操作(点对点消息 → 流式处理)
- 消费端是 Flow:channel.consumeAsFlow() 或 receiveAsFlow(),再接 conflate()/sample() 做 UI 限频。
- 生产端是 Flow:flow.buffer(capacity, onBufferOverflow) 与 Channel 语义一致;flowOn 在边界处也会插入 BUFFERED 通道解耦。
val uiFlow = ch.receiveAsFlow()
.conflate() // UI 只要最新
.sample(200) // 每 200ms 刷一次
.flowOn(Dispatchers.Default)
7) 选择指南(背压/容量小抄)
| 目标 | 推荐配置 |
|---|---|
| 零丢失 + 强背压 | capacity=N + SUSPEND(或 RENDEZVOUS) |
| 只关心最新 | CONFLATED 或 capacity=N + DROP_OLDEST + trySend |
| 有限滑窗 + 高吞吐 | capacity=32/64/128 + DROP_OLDEST |
| 不建议 | UNLIMITED(隐性堆积→OOM) |
8) 常见坑
- 生产者阻塞不可见:SUSPEND 下 send 会挂起——请监控队列长度;必要时切换 DROP_OLDEST 或增大容量。
- UNLIMITED 伪稳:吞吐看似稳定,实则把压力堆进内存。
- 未清理未送达元素:复杂元素(文件句柄/Bitmap)务必提供 onUndeliveredElement 释放资源。
- close vs cancel:close() 让接收者把已入队的读完;cancel() 直接丢弃未读。
- 多消费者“抢占” :同一 Channel 多消费者读取是竞争关系,不保证均衡/顺序;需要均衡请自建调度。
9) 最小演示:同一套代码,切换三种背压策略
suspend fun demo(overflow: BufferOverflow, cap: Int) = coroutineScope {
val ch = Channel<Int>(capacity = cap, onBufferOverflow = overflow)
val p = launch(Dispatchers.Default) {
repeat(1000) { i ->
ch.trySend(i).onFailure { ch.send(i) } // 先尝试不阻塞发送,不成再挂起发送
}
ch.close()
}
val c = launch {
for (v in ch) {
delay(5) // 模拟慢消费
}
}
joinAll(p, c)
}
runBlocking {
demo(BufferOverflow.SUSPEND, 32) // 绝不丢,慢就顶住
demo(BufferOverflow.DROP_OLDEST, 32) // 滑窗,只保近数据
demo(BufferOverflow.DROP_LATEST, 32) // 保已有数据,丢新来的
}
结论
- Channel = 协程间点对点消息邮筒;“容量 + 溢出策略”决定背压。
- 业务要“必达” :用 SUSPEND(或 RENDEZVOUS)形成强背压;业务要“最新优先” :用 CONFLATED 或 DROP_OLDEST。
- 把 Channel 串成多段流水线,每一段容量都是你的节流阀;必要时用 onUndeliveredElement 做资源清理,用 receiveAsFlow() 接上 Flow 的限频/平滑生态。