channels是.net core 3.0+线程安全、无锁的生产者-消费者原语,支持异步流、背压、取消和多消费者分发;选bounded需控缓冲与背压,unbounded仅适用于写入即忘且消费匹配的轻量场景。

Channels 是 .NET Core 3.0+ 中真正可用、线程安全、无锁(多数路径)的生产者-消费者原语,不是 Task.Run 包一层 BlockingCollection 的替代品——它天然支持异步流、背压、取消和多消费者分发。
Channel.CreateBounded() 和 Channel.CreateUnbounded() 怎么选
关键看是否需要显式控制缓冲区与背压行为:
-
Channel.CreateBounded<t>(capacity: 100)</t>:写入时若缓冲满,Writer.WriteAsync()默认会 await 挂起(可配FullMode改为丢弃或抛异常),适合 I/O 密集型场景(如从网络包队列中节流解析) -
Channel.CreateUnbounded<t>()</t>:写入永不阻塞,但内存不受控——大量快速生产 + 慢速消费会导致 OOM;仅适合“写入即忘”且消费速率基本匹配的轻量内部通信(如事件总线中瞬时日志转发) - 注意:
Unbounded的Reader.TryRead()仍可能返回 false(通道已关闭且空),别误以为“永远能读”
为什么 ChannelReader.ReadAsync() 要用 while (await reader.WaitToReadAsync()) 循环
因为 WaitToReadAsync() 只表示“此刻可能有数据可读”,不保证 TryRead() 一定成功——尤其在多消费者竞争或写入端批量 WriteAsync 后部分被取走时:
while (await reader.WaitToReadAsync(ct))
{
while (reader.TryRead(out var item))
{
Process(item);
}
}
- 外层
WaitToReadAsync()防止忙等;内层TryRead()循环是为了清空当前可用批次,避免每次只处理一个再等下一次唤醒(吞吐暴跌) - 漏掉内层循环是常见性能坑:QPS 可能直接跌 5–10 倍
- 如果业务逻辑允许批处理,可改用
reader.ReadAllAsync(ct)(.NET 6+),它自动聚合为IAsyncEnumerable<t></t>
ChannelWriter.Complete() 后还能不能 WriteAsync
不能。调用 Complete() 或 Complete(Exception) 后,任何后续 WriteAsync() 都会立即抛出 InvalidOperationException:“Channel has been completed.”
- 常见错误:在
try/finally中Complete(),但生产逻辑里有未捕获异常导致提前退出,结果WriteAsync()在Complete()前就失败了——此时消费者收到的是“突然中断”,而非优雅终止 - 正确做法:把
Complete()放在生产逻辑结束后(非 finally),并确保所有WriteAsync()都 await 且处理OperationCanceledException - 想中途停止生产又不让消费者报错?用
cancellationToken传给WriteAsync(),消费者侧监听reader.Completion任务即可感知完成
最易被忽略的是 Channel 的生命周期绑定——它不自动绑定到 CancellationTokenSource,也不参与 IServiceProvider 的 DI 生命周期管理。手动 Dispose() 不解决资源泄漏,必须确保 Writer.Complete() 被调用,且所有 Reader.ReadAsync() 任务结束,否则底层 ConcurrentQueue 或 SpinWait 状态可能残留。











