应使用 Channel.CreateBounded 控制内存水位防OOM,如日志采集中生产5000/s、消费800/s时设Capacity=1000,满时可Wait/DropOldest等;CreateUnbounded非无限内存,仅简化背压,需谨慎避免持续积压。

什么时候该用 Channel.CreateBounded?
当你需要控制内存水位、防止生产者把消费者“拖垮”时,必须选有界通道。比如:实时日志采集服务中,上游每秒写入 5000 条事件,下游单个消费者每秒最多处理 800 条——这时若用无界通道,几秒内就可能堆积数万条未消费消息,OutOfMemoryException 风险极高。
-
BoundedChannelOptions必须指定Capacity(如new BoundedChannelOptions(1000)),这是硬上限 - 满时行为由
FullMode决定:Wait(默认,写操作挂起)、DropOldest(丢老数据)、DropNewest(丢新数据)或DropWrite(直接返回 false 不阻塞) - 若设
SingleWriter = true,可省去内部锁开销,适合单线程生产场景 - 注意:
Capacity是元素个数,不是字节数;装的是string还是byte[],内存占用差异巨大
为什么 Channel.CreateUnbounded 不等于“随便用”?
它只是没有显式容量限制,但不意味着能无限吃内存。底层仍是托管堆上的对象集合,一旦生产速度持续高于消费速度,GC 压力飙升,最终照样 OOM。它真正的价值在于简化背压逻辑,而非纵容失控。
-
UnboundedChannelOptions支持SingleReader和AllowSynchronousContinuations等调优项,但没Capacity字段 -
AllowSynchronousContinuations = false可避免回调在 I/O 线程上同步执行,防止线程池饥饿(尤其在 ASP.NET Core 中很重要) - 典型适用场景:短生命周期任务管道(如 HTTP 请求进 → 验证 → 缓存检查 → 响应),全程耗时
- 误用高发点:拿它替代消息队列(如 RabbitMQ/Kafka)做跨进程/跨机器通信——
Channel仅限进程内,崩溃即丢失
WriteAsync 在两种模式下行为差异极大
表面看都是 await 写入,但背后调度逻辑完全不同:
- 有界通道下,
await channel.Writer.WriteAsync(x)可能**长时间挂起**(取决于FullMode)。例如FullMode.Wait时,若队列已满,协程会挂起直到消费者取走至少一个元素 - 无界通道下,
WriteAsync几乎总能立即返回(除非内存彻底耗尽),但代价是:你失去了对写入节奏的主动控制权 - 若想统一处理“写失败”,有界通道可配合
TryWrite(非 await,返回 bool);无界通道没有等效 API,只能靠异常捕获或提前监控内存 - 务必记得:无论哪种模式,写完都要调用
channel.Writer.Complete(),否则ReadAllAsync永远不会结束
别忽略 Reader/Writer 的并发安全配置
默认创建的 Channel 允许多生产者多消费者,但如果你实际只用单线程生产+单线程消费,却没关掉多余同步开销,性能反而下降。
- 有界通道中,
SingleWriter = true+SingleReader = true可让内部跳过大部分原子操作和锁,吞吐量提升 20%~40% - 无界通道中,
SingleReader = true同样有效,但SingleWriter = true效果有限(因无界实现本身已偏向无锁) - 错误示范:
Channel.CreateBounded什么选项都不设,结果在高并发写入时出现(100) InvalidOperationException: Channel is closed—— 很可能是多个 Writer 同时调用Complete()导致的竞态
Bounded 还是 Unbounded,而是预估好你的 Capacity 值和 FullMode 策略是否匹配业务语义。比如金融交易系统丢数据不可接受,那宁可 Wait 卡住也不能 DropOldest;而监控指标上报丢了最近几秒数据问题不大,但卡住会导致整个采集链路雪崩。










