c# Channel 的 Bounded 和 Unbounded 模式有什么区别

应使用 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 支持 SingleReaderAllowS

    ynchronousContinuations
    等调优项,但没 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;而监控指标上报丢了最近几秒数据问题不大,但卡住会导致整个采集链路雪崩。