blockingcollection 适用于同步线程阻塞模型,其 add() 和 take() 在队列满或空时会挂起线程等待;channel 适用于异步 i/o 流控,writeasync()/readasync() 基于 await 实现非阻塞等待。

选 Channel<t></t> 还是 BlockingCollection<t></t>,不看“新旧”,只看你的阻塞模型是否匹配:同步线程等待就用后者,异步 I/O 流控就用前者。混用或强行转译,八成会卡死、OOM 或丢数据。
BlockingCollection.Add() 在 async 方法里为什么总卡住?
它本质是同步阻塞操作,调用 Add() 时线程真会停住等空间,放进 async 方法却没配 Task.Run,就会把线程池线程钉死——尤其在 ASP.NET Core 这类受限线程池场景下,几秒内就耗尽可用线程。
- 别在
async方法体里直接调用Add()或Take() - 如果非要用,必须包进
Task.Run(() => collection.Add(item)),但这是权宜之计,不是解法 -
GetConsumingEnumerable()是同步迭代器,不能用在await foreach里;误用会导致死锁,不是语法错误 - 调试时可查
collection.Count和collection.IsAddingCompleted,但线上别靠它做流控逻辑
Channel.Writer.WriteAsync() 为什么有时不返回、也不报错?
默认有界通道(Channel.CreateBounded<t>(capacity)</t>)满时,WriteAsync() 会 await 等待空位,而不是失败——这没问题;但如果你忘了调用 writer.Complete(),或者 Writer 实例被提前 GC(.NET 6+ 不实现 IDisposable,不会自动 complete),Reader.ReadAsync() 就永远挂起,既不抛异常也不返回。
- 所有
WriteAsync()调用都应包裹在try/catch中,并在finally块里确保writer.Complete() - 不要依赖析构器或
using语句自动清理ChannelWriter<t></t> - 用
await reader.WaitToReadAsync(ct)配合reader.TryRead(out var item)替代裸ReadAsync(),更可控 -
ReadAsync()在 Channel 关闭后读空会抛InvalidOperationException,不是返回default(T)
有界 vs 无界 Channel 怎么选才不 OOM?
Channel.CreateUnbounded<t>()</t> 看似省心,实则是内存炸弹——它底层用 ConcurrentQueue<t></t>,写入永不阻塞,一旦消费者处理变慢(比如磁盘 IO 卡顿、GC 暂停),未消费项就在内存里堆叠,几万条小对象就能吃掉几百 MB。
- 日志采集、传感器采样等高频率、可丢弃场景:用
Channel.CreateBounded<t>(new BoundedChannelOptions(1000) { FullMode = BoundedChannelFullMode.DropWrite })</t> - 订单、支付等强一致性场景:用
BoundedChannelFullMode.Wait(默认),靠背压让上游降速,容量设为合理上限(如 500–2000) - 绝对避免
CreateUnbounded处理未知速率的生产流,除非你已监控channel.Reader.Count并配了告警 -
TryWrite()默认不返回false,只在FullMode = DropWrite/Latest下才可能;误判它“失败”常源于 Writer 已被Complete()或异常终结
真正容易被忽略的点是:Channel 的生命周期管理不是自动的。Writer 不 complete,Reader 就永远等;Reader 不主动退出或取消,任务就一直挂着。这不是 API 设计缺陷,而是它把控制权交还给了你——写错一行 Complete(),整条流水线就静默瘫痪。











