channel是构建高吞吐、可取消、背压可控后台任务队列的首选,它通过内置缓冲与阻塞机制天然支持背压,而concurrentqueue+task.run易遗漏取消、满载处理及竞态问题。

用 Channel<t></t> 做后台任务队列,比 ConcurrentQueue<t></t> + Task.Run 更可靠
直接说结论:C# 6 之后,Channel<t></t> 是构建高吞吐、可取消、背压可控的后台任务队列的首选。它不是“更高级的教程技巧”,而是解决真实并发问题的基础设施——比如你发了 1000 个日志写入请求,但磁盘 I/O 只能扛住每秒 50 次,Channel<t></t> 能天然缓冲 + 阻塞生产者,而手动用 ConcurrentQueue<t></t> 很容易漏掉取消、满载丢弃或竞态判断。
Channel.CreateBounded<t></t> 的容量设置决定行为边界
容量不是随便填的数字,它直接影响队列是否阻塞、是否丢弃、是否抛异常:
-
Channel.CreateBounded<int>(10)</int>:写满 10 个后,Writer.TryWrite()返回false;await Writer.WriteAsync()会挂起,直到有空位 -
Channel.CreateBounded<int>(new BoundedChannelOptions(10) { FullMode = ChannelFullMode.DropWrite })</int>:写入时自动丢弃最老项(FIFO),适合监控采样类场景 -
Channel.CreateUnbounded<int>()</int>:无内存保护,大量积压可能 OOM;仅适合瞬时爆发小、处理极快的场景
别设 0 或负数——Channel 不支持零容量,会抛 ArgumentException。
后台消费者必须用 async Task + Reader.ReadAsync(),不能用同步循环
ChannelReader<t></t> 的设计前提是异步流式消费。常见错误是写成:
while (reader.TryRead(out var item)) { /* 处理 */ }
这会跳过所有尚未写入的项,且无法响应取消。正确模式是:
- 启动一个
async Task方法,在里面await foreach (var item in reader.ReadAllAsync(cancellationToken)) - 或手动循环:
while (await reader.WaitToReadAsync(cancellationToken)) { if (reader.TryRead(out var item)) { /* 处理 */ } } - 务必传入
CancellationToken,否则WaitToReadAsync和ReadAsync无法被外部中断
关闭通道要分两步:Writer.Complete() + 等待消费者退出
Writer.Complete() 只是告诉读者“不会再有新数据”,不会等待正在处理的项完成,也不会终止消费者任务。常见坑:
- 调用
Writer.Complete()后立刻await consumerTask,但消费者还在处理最后一项,导致超时或未处理完 - 没在消费者里捕获
OperationCanceledException,导致cancellationToken触发后任务崩溃 - 多个生产者共用一个
Writer,只对其中一个调用Complete(),其余仍可写入(Channel不强制单生产者)
真正安全的关闭流程是:通知所有生产者停写 → 调用 Writer.Complete() → await consumerTask → 再 dispose channel 相关资源。
最易被忽略的是背压传递——如果你的消费者处理慢,而生产者不检查 TryWrite 返回值或不 await WriteAsync,那背压就断在了第一环。这不是 Channel 的缺陷,而是你绕过了它的设计契约。











