1. 三类接口的职责与关系
类型 | 职责 | 方向 | 典型场景 |
---|---|---|---|
SendChannel | 只负责发送 | send(T) ➜ 通道 | 把“写端”暴露给调用方(不让它读) |
ReceiveChannel | 只负责接收 | 通道 ➜ receive() | 把“读端”暴露给调用方(不让它写) |
Channel | 读写二合一 | 双向 | 在内部或同域同时读写 |
-
继承关系:Channel : SendChannel, ReceiveChannel。
-
设计目的:解耦方向。很多 API 只需要“写端”或“读端”,暴露窄接口更安全(防误用)。
2. 类型变型(非常重要)
-
SendChannel 逆变:需要发送 String 时,你可以传入 SendChannel 当作参数(Any 能接收 String)。
结论:SendChannel 是 SendChannel 的子类型。
-
ReceiveChannel 协变:有一个 ReceiveChannel,可当成 ReceiveChannel(读出来的 String 也是 Any)。
结论:ReceiveChannel 是 ReceiveChannel 的子类型。
这让“只写端/只读端”的 类型安全 与 复用 非常自然。
3. 常用 API 速览(现代写法)
发送侧(SendChannel)
-
挂起发送:send(value) — 缓冲满/无接收者时挂起,直到可写。
-
非挂起尝试:trySend(value): ChannelResult
- .isSuccess / .isFailure;失败可能是满或已关闭。
- 阻塞场景(非协程线程)可用 trySendBlocking(value).
-
状态:isClosedForSend。
-
结束:close(cause: Throwable? = null)(优雅关闭写端)。
接收侧(ReceiveChannel)
-
挂起接收:receive(): T(通道已关且空时抛 ClosedReceiveChannelException)。
-
非挂起尝试:tryReceive(): ChannelResult。
-
推荐安全接收:receiveCatching(): ChannelResult
- 成功:.getOrNull();已关闭:.isClosed,.exceptionOrNull() 取关闭原因。
-
遍历:for (e in channel) 会在“读尽且关闭”时自然结束。
-
状态:isClosedForReceive(注意:只有在读完缓冲后才会变 true)。
旧版 offer/poll/consumeEach 等已过时或不推荐,用上面三兄弟即可。
4. 缓冲与背压(Channel(capacity))
-
RENDEZVOUS(容量 0):严格背压,send 必须等有 receive。
-
BUFFERED(默认缓冲,大小依实现/平台而定,通常 64):适度吸收生产突刺。
-
CONFLATED:只保留最新元素(状态类消息/热信号,很像 StateFlow)。
-
UNLIMITED:理论无限队列,小心 OOM。
背压策略直接决定 send/receive 的挂起时机,影响吞吐与内存占用。
5. 生命周期:close vs cancel
-
close(cause?)(发送侧调用):
- 阻止后续发送;保留/允许把缓冲区剩余元素读完;接收侧用 receiveCatching() 能看到关闭并读尽。
-
cancel(cause?)(任意一侧):
- 立即失败并丢弃未读元素,所有挂起操作以 CancellationException 结束。
-
经验法则:谁负责生产,谁负责 close;消费者若不想再读,用 cancel() 终止消费并释放资源。
6. 所有权与结构化并发(最佳实践)
-
通道应作为“协程之间”的单向资源:
1)生产者创建 Channel;
2)把 只读端 ReceiveChannel 交给消费者;
3)生产结束时 生产者 close;
4)消费者用 for (e in ch) 或 receiveCatching() 自然退出。
-
放到 coroutineScope {} 中,确保异常能级联取消,避免泄漏。
7. 典型用法示例
7.1 一生产一消费(优雅关闭)
val ch = Channel<Int>(capacity = Channel.BUFFERED)
val producer = launch {
try {
repeat(100) { i ->
ch.send(i) // 缓冲满则挂起
}
} finally {
ch.close() // 生产者负责 close
}
}
val consumer = launch {
for (x in ch) { // 读尽+关闭 => 正常退出
println("got $x")
}
}
7.2 多生产者聚合(fan-in)
val ch = Channel<String>(Channel.BUFFERED)
val workers = List(3) { idx ->
launch {
repeat(5) { j ->
ch.trySend("[$idx] $j").onFailure {
// 满/关闭时可做降级或重试
}
}
}
}
val sink = launch {
for (msg in ch) println(msg)
}
// 等待所有生产者结束后关闭写端
launch {
workers.joinAll()
ch.close()
}
7.3 多消费者分发(fan-out)
val ch = Channel<Job>(Channel.BUFFERED)
// 3 个消费者“抢任务”
repeat(3) { c ->
launch {
for (task in ch) {
task.run()
}
}
}
// 生产若干任务
launch {
repeat(100) { i -> ch.send(Job(i)) }
ch.close()
}
7.4 与超时/合并事件(select简述)
select {
ch.onReceiveCatching { res ->
if (res.isSuccess) handle(res.getOrNull()!!)
else println("closed")
}
onTimeout(200) {
println("tick")
}
}
select 适合“多个通道/事件就绪哪个先来”的竞态合并。
8. 与 Flow 的关系(只需一个心智模型)
-
ReceiveChannel.consumeAsFlow() 可以转成 Flow;
-
状态/背压:Flow 更适合“声明式、冷流、链式变换”;Channel 是“热流/总线、主动推送、需要手动管理生命周期与背压**”的工具。很多时候用 Flow/SharedFlow/StateFlow 就足够,只有当你需要“协程之间点对点消息 + 明确背压/容量控制”时,才选 Channel。
9. 常见坑 & 排错清单
- 卡死/不出数:RENDEZVOUS 下没有接收者时,send 会挂起;或缓冲已满。用 trySend 观察结果,或加日志看是否卡在 send/receive。
- 关闭读取不到:只用 receive() 会在“关且空”时抛异常;更推荐 receiveCatching() 或 for (e in ch) 循环。
- 谁来 close:始终由生产者 close;消费者要中断消费,用 cancel()。
- 无限缓冲导致内存涨:避免 UNLIMITED;用 BUFFERED/CONFLATED 或把生产速率控住。
- 混用多个代理(网络问题类比) :在 app 里混用多条通道作为总线容易乱,明确边界:一个职责一条通道,读写端尽量只暴露需要的接口。
- 异常传播:协程取消会让通道操作抛 CancellationException,注意 try/finally 释放资源。