.NET 官方`System.Threading.Channels`

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 链表加锁),吞吐更高。

六、边界条件(容易踩的坑)

  1. 忘了 Complete()ReadAllAsync 永远不结束,Task.WhenAll 死等。这是最常见的 bug。
  2. 多写者各自 Complete() → 第 2 个 Complete 无效(TryComplete 返回 false);必须等所有生产者 Task 完成后由主控统一 Complete
  3. Complete + 队列还有数据 → 消费者先把剩余数据读完,再读才抛 ChannelClosedException
  4. Complete(ex) → 消费端在 ReadAllAsync 里直接抛出原始异常(可用 Completion.IsFaulted 判断)。
  5. 队列空且未 CompleteReadAsync 永久挂起,只能靠 CancellationToken 取消。
  6. 多消费者不是广播 :一条消息只被一个消费者消费。要"发给所有人"得自己做(每个消费者单独一个 channel),或用 TPL Dataflow 的 BroadcastBlock
  7. Count 只在 CanCount == true 时可信:bounded 为 true;unbounded 为 false,别依赖。
  8. 取消的 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&lt;T&gt;</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&lt;T&gt;</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&lt;T&gt;</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 } 一接就通,采得快也不怕,自动背压保护内存。需要我对某个场景(比如串口采集 + 数据处理 + 存库)写一个完整工程,直接说。

相关推荐
词却1 小时前
数据库基础:DQL 数据查询语言
数据库·oracle
计算机魔术师1 小时前
GPT-6 Astra幻觉砍到2%,却被一种老招数轻松绕过
前端
cjy0001112 小时前
2026AI 产品如何从一次性 Demo 变成可重复使用的业务工具?
前端·人工智能·fde
IT_陈寒2 小时前
React的状态更新坑得我差点加班到天亮
前端·人工智能·后端
风骏时光牛马2 小时前
深挖底层逻辑:XX源码深度拆解分析
前端
渡我白衣2 小时前
并查集:基础认识与模拟实现
android·java·javascript·数据结构·c++·算法·并查集
电商API_180079052472 小时前
电商商品价格监控工具淘宝京东商品价格抓取API项目实操分享
java·大数据·开发语言·前端·数据挖掘
sxmas3 小时前
用状态机与DAG建模四阶段业务流程:以MIND模型为例
前端
昌原的儿子LEO3 小时前
Linux IO多路复用与SQLite数据库核心知识点总结
linux·数据库·oracle