System.Threading.Channels 是 .NET 官方对生产者---消费者模型 的标准实现(.NET Core 3.0+)。核心就一句话:一个线程安全的内存队列,把"写的一方"和"读的一方"完全解耦,且带异步背压。下面按 创建 → 生产者 API → 消费者 API → 完整示例 → 原理/边界 讲透。
一、创建 Channel(两种)
using System.Threading.Channels;
// ① 无界:不阻塞写,内存可无限增长(谨慎,只适合低速率)
var unbounded = Channel.CreateUnbounded<int>();
// ② 有界:容量 100,满时写端被"卡住"(背压),这才是生产者-消费者的精髓
var bounded = Channel.CreateBounded<int>(new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.Wait, // 满时的行为,见下表
SingleReader = false, // 声明"只有单个消费者"可获性能优化
SingleWriter = false, // 声明"只有单个生产者"可获性能优化
AllowSynchronousContinuations = false // 一般不开启
});
FullMode(只有有界才有意义)是背压策略的关键:
| FullMode | 队列满时写端行为 |
|---|---|
Wait(默认) |
WriteAsync 挂起等待,直到消费者腾出空间(标准背压) |
DropOldest |
丢弃最旧一条,写入新消息,写端不等待 |
DropNewest |
丢弃最新一条,写入新消息,写端不等待 |
DropWrite |
直接丢弃"正要写的这条",写端不等待 |
二、生产者 API(ChannelWriter)
var w = channel.Writer;
await w.WriteAsync(item); // 异步写;bounded+Wait 时满则挂起(背压)
w.TryWrite(item); // 非阻塞写;满返回 false(用 Drop* 模式时会丢弃并返回 true)
await w.WaitToWriteAsync(); // 等待"现在可写"的信号,再配合 TryWrite
w.Complete(); // 通知消费者:没有更多数据了(读端 ReadAllAsync 会正常结束)
w.TryComplete(); // 同上,但可安全多次调用,返回 bool 表示是否本次生效
w.Complete(new Exception("...")) // 带异常完成 → 消费端抛该异常
三、消费者 API(ChannelReader)
var r = channel.Reader;
var item = await r.ReadAsync(ct); // 异步读一条;空时挂起,已完成后抛 ChannelClosedException
r.TryRead(out var x); // 非阻塞读,成功返回 true
await foreach (var it in r.ReadAllAsync(ct)) // 最常用:流式读,直到 Complete()
await r.WaitToReadAsync(); // 等待"有数据可读"
r.Count / r.CanCount // 元素数;bounded 精确,unbounded 不可用
r.Completion // Task:Channel 完成时完成
四、完整可运行示例
示例 1:经典 1 生产者 + 1 消费者
using System.Threading.Channels;
var ch = Channel.CreateBounded<int>(new BoundedChannelOptions(64)
{
FullMode = BoundedChannelFullMode.Wait,
SingleWriter = true, // 确认只有一个写者,去掉锁开销
SingleReader = true
});
var producer = Task.Run(async () =>
{
for (int i = 1; i <= 1000; i++)
{
await ch.Writer.WriteAsync(i); // 满 64 时自动挂起 → 节流
await Task.Delay(1); // 模拟采集速率
}
ch.Writer.Complete(); // 通知结束(一定要调!)
});
var consumer = Task.Run(async () =>
{
await foreach (var n in ch.Reader.ReadAllAsync())
{
Console.WriteLine($"处理 {n}"); // 模拟耗时处理
await Task.Delay(2);
}
});
await Task.WhenAll(producer, consumer);
Console.WriteLine("全部完成");
示例 2:多生产者 + 多消费者(并行负载均衡)
using System.Threading.Channels;
var q = Channel.CreateBounded<string>(new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.Wait
});
// 3 个生产者
var producers = Enumerable.Range(1, 3).Select(id => Task.Run(async () =>
{
for (int i = 1; i <= 200; i++)
await q.Writer.WriteAsync($"P{id}-{i}");
}));
// 等所有生产者写完,再统一 Complete(多写者绝不能各自 Complete)
await Task.WhenAll(producers);
q.Writer.Complete();
// 5 个消费者,一条消息只被一个消费者拿到(天然负载均衡,非广播)
var consumers = Enumerable.Range(1, 5).Select(_ => Task.Run(async () =>
{
await foreach (var msg in q.Reader.ReadAllAsync())
{
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] 消费 {msg}");
await Task.Delay(10); // 模拟处理
}
}));
await Task.WhenAll(consumers);
示例 3:带取消 + 异常 + 优雅关停(上位机最实用)
using System.Threading.Channels;
var queue = Channel.CreateBounded<Job>(new BoundedChannelOptions(20)
{
FullMode = BoundedChannelFullMode.Wait
});
using var cts = new CancellationTokenSource();
// 采集线程(生产者):串口/网口/PLC 数据
var producer = Task.Run(async () =>
{
try
{
while (!cts.IsCancellationRequested)
{
var data = await ReadDeviceAsync(); // 阻塞读设备
await queue.Writer.WriteAsync(new Job(data), cts.Token); // 背压
}
}
catch (OperationCanceledException)
{
// 正常退出
}
catch (Exception ex)
{
queue.Writer.Complete(ex); // 关键:把故障传给消费端
return;
}
queue.Writer.Complete();
});
// 处理线程(消费者):多路并行处理
var consumer = Task.Run(async () =>
{
try
{
await foreach (var job in queue.Reader.ReadAllAsync(cts.Token))
{
Process(job);
}
}
catch (ChannelClosedException) when (queue.Reader.Completion.IsFaulted)
{
// 生产者侧出错了,从 Completion 里取原始异常
var inner = queue.Reader.Completion.Exception!.InnerException;
Console.WriteLine($"生产者故障: {inner}");
}
});
// 3 秒后模拟停止
await Task.Delay(3000);
cts.Cancel();
await Task.WhenAll(producer, consumer);
五、底层原理(为什么它能背压)
Channel 内部不是简单的 Queue + lock,而是线程安全存储 + 异步挂起队列(waiter list):
- 存储层 :无界用
ConcurrentQueue;有界用内部定长环形存储。 - 等待层 :写端满了,
WriteAsync不会死循环自旋,而是把自己的TaskCompletionSource(实际是AsyncOperation<T>)挂进写者等待链表 ,直接让出线程(await);消费者取走元素后,会去唤醒链表里排队的写者。反过来,读端空了同理,读者挂进读者等待链表,被写端唤醒。 - 背压的本质 :队列满时生产者的
await被阻塞,消费速率反向传导给生产速率 ,慢消费自然拖慢快生产,内存不会爆。这就是FullMode.Wait的核心价值。 - SingleWriter/SingleReader 优化:确认单写单读后,走无锁的快路径(避免对 waiter 链表加锁),吞吐更高。
六、边界条件(容易踩的坑)
- 忘了
Complete()→ReadAllAsync永远不结束,Task.WhenAll死等。这是最常见的 bug。 - 多写者各自
Complete()→ 第 2 个Complete无效(TryComplete返回 false);必须等所有生产者Task完成后由主控统一Complete。 - 已
Complete+ 队列还有数据 → 消费者先把剩余数据读完,再读才抛ChannelClosedException。 Complete(ex)后 → 消费端在ReadAllAsync里直接抛出原始异常(可用Completion.IsFaulted判断)。- 队列空且未 Complete →
ReadAsync永久挂起,只能靠CancellationToken取消。 - 多消费者不是广播 :一条消息只被一个消费者消费。要"发给所有人"得自己做(每个消费者单独一个 channel),或用 TPL Dataflow 的
BroadcastBlock。 Count只在CanCount == true时可信:bounded 为 true;unbounded 为 false,别依赖。- 取消的
WriteAsync(ct)不会写入消息,但若已成功入队则无法撤回。
七、机制全景图
<html style="margin:0;padding:0;">
<div style="background-color:transparent;box-sizing:border-box;font-family:'Roboto','PingFang SC','Segoe UI',Arial,sans-serif;color:#1A1B1C;line-height:1.5;">
<!-- 标题 -->
<div style="font-size:15px;font-weight:600;margin-bottom:10px;">Channel 内部机制:生产者 → 队列 → 消费者(含背压回路)</div>
<!-- 三栏主体 -->
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:stretch;">
<!-- 生产者栏 -->
<div style="flex:1 1 150px;min-width:0;padding:10px 12px;background:linear-gradient(135deg,rgba(163,213,232,0.18),rgba(163,213,232,0.30));border-radius:12px;box-sizing:border-box;">
<div style="font-size:12px;font-weight:600;margin-bottom:4px;">ChannelWriter<T></div>
<div style="font-size:11px;color:#6B7280;">WriteAsync ✓</div>
<div style="font-size:11px;color:#6B7280;">TryWrite(非阻塞)</div>
<div style="font-size:11px;color:#6B7280;">WaitToWriteAsync</div>
<div style="font-size:11px;color:#6B7280;">Complete / TryComplete</div>
</div>
<!-- Channel 内部 -->
<div style="flex:2 1 260px;min-width:0;padding:10px 12px;background:#FFFFFF;border:1px solid rgba(139,200,234,0.55);border-radius:12px;box-sizing:border-box;">
<div style="font-size:12px;font-weight:600;margin-bottom:4px;">Channel 内部存储</div>
<div style="font-size:11px;color:#6B7280;">· 无界:ConcurrentQueue<T></div>
<div style="font-size:11px;color:#6B7280;">· 有界:定长环形存储(容量 N)</div>
<div style="font-size:11px;color:#6B7280;">· 写者等待链表(满时挂起)</div>
<div style="font-size:11px;color:#6B7280;">· 读者等待链表(空时挂起)</div>
<div style="font-size:11px;color:#6B7280;">· SingleWriter/SingleReader 无锁快路径</div>
</div>
<!-- 消费者栏 -->
<div style="flex:1 1 150px;min-width:0;padding:10px 12px;background:linear-gradient(135deg,rgba(163,213,232,0.18),rgba(163,213,232,0.30));border-radius:12px;box-sizing:border-box;">
<div style="font-size:12px;font-weight:600;margin-bottom:4px;">ChannelReader<T></div>
<div style="font-size:11px;color:#6B7280;">ReadAsync ✓</div>
<div style="font-size:11px;color:#6B7280;">ReadAllAsync(流式)</div>
<div style="font-size:11px;color:#6B7280;">TryRead(非阻塞)</div>
<div style="font-size:11px;color:#6B7280;">WaitToReadAsync / Completion</div>
</div>
</div>
<!-- 背压回路 -->
<div style="margin-top:10px;padding:10px 12px;background:rgba(0,0,0,0.025);border:1px solid rgba(0,0,0,0.08);border-radius:12px;box-sizing:border-box;">
<div style="font-size:12px;font-weight:600;margin-bottom:6px;">背压回路(FullMode.Wait)</div>
<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;">
<div style="font-size:11px;color:#6B7280;padding:6px 10px;background:#FFFFFF;border-radius:8px;">生产者 WriteAsync</div>
<div style="font-size:12px;color:#EA6668;">→</div>
<div style="font-size:11px;color:#6B7280;padding:6px 10px;background:#FFFFFF;border-radius:8px;">队列满 → 挂起进写者等待链表</div>
<div style="font-size:12px;color:#EA6668;">→</div>
<div style="font-size:11px;color:#6B7280;padding:6px 10px;background:#FFFFFF;border-radius:8px;">消费者取走 1 条</div>
<div style="font-size:12px;color:#52C41A;">→</div>
<div style="font-size:11px;color:#6B7280;padding:6px 10px;background:#FFFFFF;border-radius:8px;">唤醒队首写者继续写</div>
</div>
<div style="font-size:11px;color:#6B7280;margin-top:6px;">效果:慢消费拖慢快生产,内存不爆------这就是生产者-消费者模型的节流核心。</div>
</div>
<!-- 生命周期 -->
<div style="margin-top:10px;padding:10px 12px;background:#FFFFFF;border:1px solid rgba(139,200,234,0.5);border-radius:12px;box-sizing:border-box;">
<div style="font-size:12px;font-weight:600;margin-bottom:6px;">生命周期(顺序不能乱)</div>
<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;">
<div style="font-size:11px;color:#6B7280;padding:6px 10px;background:rgba(163,213,232,0.2);border-radius:8px;">生产</div>
<div style="font-size:12px;color:#6B7280;">→</div>
<div style="font-size:11px;color:#6B7280;padding:6px 10px;background:rgba(163,213,232,0.2);border-radius:8px;">消费剩余数据</div>
<div style="font-size:12px;color:#6B7280;">→</div>
<div style="font-size:11px;color:#6B7280;padding:6px 10px;background:rgba(163,213,232,0.2);border-radius:8px;">Complete()</div>
<div style="font-size:12px;color:#6B7280;">→</div>
<div style="font-size:11px;color:#6B7280;padding:6px 10px;background:rgba(163,213,232,0.2);border-radius:8px;">读端 ReadAllAsync 正常结束</div>
<div style="font-size:12px;color:#6B7280;">→</div>
<div style="font-size:11px;color:#EA6668;padding:6px 10px;background:#FFFFFF;border-radius:8px;border:0.5px solid rgba(0,0,0,0.08);">再读 → ChannelClosedException</div>
</div>
<div style="font-size:11px;color:#6B7280;margin-top:6px;">注:Complete(Exception) 时,读端读完剩余数据后直接抛该异常。</div>
</div>
<div style="margin-top:10px;font-size:11px;color:#6B7280;">示意结构:基于 System.Threading.Channels 公开 API 与官方实现原理绘制,非精确内部数据结构图。</div>
</div>
</html>
八、一页速查
创建 Channel.CreateBounded<T>(容量 / BoundedChannelOptions)
写 WriteAsync(背压)| TryWrite(非阻塞)
读 await foreach (var x in Reader.ReadAllAsync())
结束 写端 Complete() → 读端循环自然退出
故障 写端 Complete(ex) → 读端抛 ex
多消费者 天然负载均衡(每消息只一人消费)
给你上位机场景的一句话建议 :串口/网口/PLC 采集线程当生产者、处理线程当消费者,用 Channel.CreateBounded(容量){ FullMode = Wait } 一接就通,采得快也不怕,自动背压保护内存。需要我对某个场景(比如串口采集 + 数据处理 + 存库)写一个完整工程,直接说。