从裸 Task 说起
简单的并发,开几个 Task 就能跑起来。但一旦涉及「一个地方产数据、另一个地方消费数据」,裸 Task 加上共享队列就要自己写锁、自己处理线程安全,容易出错。C# 给出了更合适的工具:Channel。
并发不是越多 Task 越好,而是把数据流向与同步边界写清楚。
什么时候用 Channel
- 生产者-消费者一边产、一边处理。
- 后台任务队列把耗时操作异步消化。
- 流式处理数据一段段过多个环节。
Channel:安全的异步队列
Channel 是一个内置线程安全的异步消息队列:写端用 WriteAsync 放入,读端用 ReadAsync 取出。它是异步的,读不到时会等待而不是空转。
var ch = Channel.CreateUnbounded<int>();
// 生产者
await ch.Writer.WriteAsync(42);
ch.Writer.Complete();
// 消费者
await foreach (var item in ch.Reader.ReadAllAsync()) {
Console.WriteLine(item);
}
有界与背压
无界队列写得快、消费得慢时,内存会无限涨。改用有界队列并配一个溢出策略,就能形成背压:队列满了,写端会等消费腾出位置,整体速度自然匹配。
var ch = Channel.CreateBounded<WorkItem>(
new BoundedChannelOptions(100) {
FullMode = BoundedChannelFullMode.Wait });
多生产者多消费者
Channel 本身线程安全,多个生产者同时写、多个消费者同时读都没问题。常见做法是起 N 个消费 Task 并行 ReadAsync,把吞吐量拉到预期水平,同时用有界队列控制总堆积。
for (int i = 0; i < 4; i++) {
_ = Task.Run(async () => {
await foreach (var w in ch.Reader.ReadAllAsync(ct))
await ProcessAsync(w);
});
}
把多步处理串成管道
复杂一点的场景,是数据要过好几步:采集 → 解析 → 计算 → 落库。用多个 Channel 把每一步连起来,每一步只关心自己的输入与输出,就拼成了一条清晰的数据流管道。
管道的好处
- 解耦每步独立、可单独替换。
- 并行各步可同时在不同数据上推进。
- 可观测每个 Channel 的堆积量就是瓶颈指标。
取消与异常处理
管道要能停。给 ReadAllAsync 传一个 CancellationToken,外层取消时所有消费端会及时退出。每一步的异常要隔离,别让一个坏数据炸掉整条管道。
小结与延伸阅读
Channel 把「并发」从手写锁的队列,变成了声明式的数据流管道。配合有界背压,并发程序既快又稳。
- 异步基础读《C# LINQ 与异步编程实战》。
- 性能与分配读《.NET 性能优化与内存管理实战》。
- 容器与模块读《.NET 依赖注入与模块化架构实战》。