从裸 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 的堆积量就是瓶颈指标。