C#生产者-消费者模式

模式概述

什么是生产者-消费者模式?

生产者-消费者模式(Producer-Consumer Pattern)是一种经典的多线程设计模式,它描述了如何协作地处理数据流。想象一个现实生活中的场景:

生活中的例子:餐厅后厨

复制代码
厨师(生产者)           传菜窗口(缓冲区)           服务员(消费者)
    ↓                         ↓                            ↓
  做菜                      放置菜品                     端给客人
    ↓                         ↓                            ↓
  持续制作                  临时存放                     持续服务

在这个场景中:

  • 厨师:负责制作菜品,不关心谁端菜,也不关心客人吃多快

  • 传菜窗口:临时存放菜品的地方,容量有限

  • 服务员:负责把菜端给客人,不关心菜怎么做的

如果厨师做得太快,窗口满了,厨师就得等一下(阻塞或丢弃) 如果服务员端得快,窗口空了,服务员就得等一下(阻塞)

为什么需要这个模式?

在软件开发中,生产者-消费者模式解决了以下问题:

  1. 解耦生产者和消费者:生产者不需要知道消费者的存在,反之亦然

  2. 平衡处理速度:当生产速度和消费速度不一致时,缓冲区可以起到缓冲作用

  3. 提高并发性:生产者和消费者可以并行工作

  4. 简化错误处理:某个环节出错不会影响其他环节

在运动控制中的重要性

运动控制系统中,生产者-消费者模式非常重要:

复制代码
上位机指令(生产者)    命令队列(缓冲区)    控制卡执行(消费者)
    ↓                         ↓                      ↓
  发送运动指令              临时存储              执行运动
    ↓                         ↓                      ↓
  可能快速发送              平滑处理              按实际速度执行

核心概念

三个核心组件

1. 生产者(Producer)
  • 职责:生成数据或任务

  • 特点

    • 独立运行,不关心消费者状态

    • 可能产生数据的速度不均匀

    • 只负责把数据放入缓冲区

  • 运动控制中的例子

    • UI 线程接收用户操作(点击按钮、参数修改)

    • 通信线程接收上位机指令

    • 定时器产生周期性数据采集任务

2. 消费者(Consumer)
  • 职责:处理数据或执行任务

  • 特点

    • 独立运行,不关心生产者状态

    • 处理速度可能慢于生产速度

    • 从缓冲区取出数据并处理

  • 运动控制中的例子

    • 运动控制线程执行运动命令

    • 数据采集线程处理传感器数据

    • 日志记录线程写入日志文件

3. 缓冲区(Buffer/Queue)
  • 职责:存储待处理的数据,连接生产者和消费者

  • 类型

    • 有界队列:容量有限,满了会阻塞生产者

    • 无界队列:容量无限,可能导致内存溢出

  • 关键特性

    • 线程安全:多个线程同时读写不会出问题

    • 阻塞/非阻塞:满了/空了时的行为

  • 运动控制中的例子

    • 运动命令队列

    • 数据采集缓冲区

    • 日志消息队列

关键问题

问题1:缓冲区满了怎么办?
  • 策略1:阻塞等待(Blocking)

    • 生产者等待,直到有空间

    • 优点:不丢失数据

    • 缺点:可能影响生产者响应

  • 策略2:丢弃最旧的(Discard Oldest)

    • 删除最早的数据,放入新的

    • 优点:总是有最新数据

    • 缺点:可能丢失重要数据

  • 策略3:拒绝接受(Reject)

    • 新数据被拒绝,返回失败

    • 优点:明确告知生产者

    • 缺点:需要生产者处理失败

问题2:缓冲区空了怎么办?
  • 策略1:阻塞等待(Blocking)

    • 消费者等待,直到有数据

    • 优点:简单可靠

    • 缺点:消费者可能被阻塞

  • 策略2:返回空(Return Null)

    • 立即返回,表示无数据

    • 优点:消费者可以继续做其他事

    • 缺点:需要消费者轮询

  • 策略3:超时等待(Timeout)

    • 等待一段时间,然后返回

    • 优点:平衡响应和效率

    • 缺点:需要处理超时

问题3:多个生产者/消费者怎么办?
  • 并发访问:缓冲区必须支持多线程并发读写

  • 公平性:所有生产者/消费者应该有平等机会

  • 性能:锁的粒度和策略会影响性能


C# 中的实现方式

方式对比

方式 适用场景 性能 复杂度 .NET版本
Channel<T> 推荐,现代应用 ⭐⭐⭐⭐⭐ .NET Core 3.0+
BlockingCollection<T> 传统应用,需要复杂控制 ⭐⭐⭐⭐ .NET 4.0+
Queue<T> + lock 简单场景,需要自定义 ⭐⭐⭐ 所有版本
ConcurrentQueue<T> 无界队列,高性能 ⭐⭐⭐⭐ .NET 4.0+

推荐选择

首选:Channel<T>

  • 最新的实现,性能最优

  • API 简洁易用

  • 支持异步操作

  • 官方推荐的生产者-消费者集合

备选:BlockingCollection<T>

  • 需要更多控制选项时

  • .NET Framework 项目

  • 已经在使用此方案的代码


Channel<T> 详解

什么是 Channel<T>?

System.Threading.Channels.Channel<T> 是 .NET Core 3.0 引入的高性能生产者-消费者集合,专为异步场景设计。

为什么选择 Channel<T>?

  1. 专为异步设计:所有操作都是异步的,不会阻塞线程

  2. 高性能:内部使用高效的数据结构和同步机制

  3. 灵活的容量控制:支持有界和无界通道

  4. 优雅的完成机制:支持优雅地通知通道关闭

  5. 官方推荐:微软官方文档推荐的生产者-消费者方案

基本用法

创建 Channel
cs 复制代码
// 方式1:创建无界通道(容量无限,直到内存耗尽)
var unboundedChannel = Channel.CreateUnbounded<string>();

// 方式2:创建有界通道(容量有限)
var boundedChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(100)
{
    FullMode = BoundedChannelFullMode.Wait  // 满了时等待
    // FullMode = BoundedChannelFullMode.DropOldest  // 满了时丢弃最旧的
    // FullMode = BoundedChannelFullMode.DropWrite  // 满了时丢弃新的
});

容量模式说明:

模式 行为 适用场景
Wait 阻塞等待直到有空间 不想丢失数据
DropOldest 丢弃最旧的数据 只关心最新数据
DropWrite 丢弃新写入的数据 保留已有数据
写入数据(生产者)
cs 复制代码
// 同步写入(会阻塞直到成功)
channel.Writer.Write("Hello");

// 异步写入(返回 Task,可以 await)
await channel.Writer.WriteAsync("Hello");

// 尝试写入(不阻塞,立即返回是否成功)
bool success = channel.Writer.TryWrite("Hello");

选择建议:

  • async 方法中,优先使用 WriteAsync

  • 如果不希望阻塞,使用 TryWrite 并处理失败

  • 在非异步代码中,可以使用 Write(但要小心阻塞)

读取数据(消费者)
cs 复制代码
// 同步读取(会阻塞直到有数据)
string item = await channel.Reader.ReadAsync();  // 实际上还是异步的

// 异步读取
string item = await channel.Reader.ReadAsync();

// 尝试读取(不阻塞)
bool success = channel.Reader.TryRead(out string item);

// 等待数据可读(不立即读取)
await channel.Reader.WaitToReadAsync();

选择建议:

  • 在消费者循环中,使用 await foreach 模式(推荐)

  • 如果只需要检查是否有数据,使用 TryRead

  • 如果需要超时控制,使用 TryRead 配合定时器

完整的消费者模式
cs 复制代码
// 推荐:使用 await foreach 遍历
await foreach (var item in channel.Reader.ReadAllAsync())
{
    // 处理数据
    Console.WriteLine($"Processing: {item}");
}

这个模式的优点:

  • 自动处理通道关闭

  • 异步高效

  • 代码简洁清晰

Channel 的关闭机制

cs 复制代码
// 完成写入端(通知消费者不会再有新数据)
channel.Writer.Complete();

// 带异常完成(表示写入过程中出错)
channel.Writer.Complete(new Exception("Something went wrong"));

// 检查是否已完成
if (channel.Writer.TryComplete())
{
    Console.WriteLine("Channel completed successfully");
}

// 消费者读取完成时会自动退出循环
// ReadAllAsync 会在通道完成后自动结束

注意事项:

  • 一旦调用 Complete(),就不能再写入数据

  • 如果消费者还在读取,可以继续读取剩余数据

  • 优雅地关闭通道是良好实践

BlockingCollection<T> 详解

什么是 BlockingCollection<T>?

System.Collections.Concurrent.BlockingCollection<T> 是 .NET 4.0 引入的线程安全集合,专为生产者-消费者场景设计。

为什么了解 BlockingCollection<T>?

虽然 Channel<T> 是更新的选择,但 BlockingCollection<T> 仍然有用:

  1. 许多现有代码使用此方案

  2. .NET Framework 项目必须使用它

  3. 提供一些 Channel<T> 没有的功能(如取消令牌)

  4. 同步场景可能更适合

基本用法

创建 BlockingCollection
cs 复制代码
// 方式1:使用默认容量(无界)
var collection = new BlockingCollection<string>();

// 方式2:指定容量(有界)
var collection = new BlockingCollection<string>(100);

// 方式3:使用自定义集合作为底层存储
var concurrentQueue = new ConcurrentQueue<string>();
var collection = new BlockingCollection<string>(concurrentQueue, 100);
写入数据(生产者)
cs 复制代码
// 添加数据(如果满了会阻塞)
collection.Add("Hello");

// 异步添加(带取消令牌)
try
{
    collection.Add("Hello", cancellationToken);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Add operation was canceled");
}

// 尝试添加(不阻塞)
bool success = collection.TryAdd("Hello");

// 尝试添加(带超时)
bool success = collection.TryAdd("Hello", TimeSpan.FromSeconds(1));
读取数据(消费者)
cs 复制代码
// 取出数据(如果空了会阻塞)
string item = collection.Take();

// 异步取出(带取消令牌)
try
{
    string item = collection.Take(cancellationToken);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Take operation was canceled");
}

// 尝试取出(不阻塞)
bool success = collection.TryTake(out string item);

// 尝试取出(带超时)
bool success = collection.TryTake(out string item, TimeSpan.FromSeconds(1));
完整的消费者循环
cs 复制代码
// 方式1:使用 GetConsumingEnumerable(阻塞直到集合完成)
foreach (var item in collection.GetConsumingEnumerable(cancellationToken))
{
    // 处理数据
    Console.WriteLine($"Processing: {item}");
}

// 方式2:手动循环(更灵活)
while (!collection.IsCompleted && !cancellationToken.IsCancellationRequested)
{
    try
    {
        string item = collection.Take(cancellationToken);
        // 处理数据
    }
    catch (OperationCanceledException)
    {
        break;
    }
}

BlockingCollection 的完成机制

cs 复制代码
// 标记为完成(不再添加数据)
collection.CompleteAdding();

// 检查是否已完成
if (collection.IsAddingCompleted)
{
    Console.WriteLine("No more items will be added");
}

// 检查是否已空且完成
if (collection.IsCompleted)
{
    Console.WriteLine("Collection is completed and empty");
}

高级功能

跨多个生产者-消费者
cs 复制代码
// 创建多个 BlockingCollection
var collection1 = new BlockingCollection<string>();
var collection2 = new BlockingCollection<string>();

// 从任意集合中读取
foreach (var item in BlockingCollection<string>.TakeFromAny(
    new[] { collection1, collection2 },
    cancellationToken))
{
    Console.WriteLine($"Received from collection: {item}");
}

这个功能允许消费者从多个生产者集合中读取,优先处理有数据的集合。


在运动控制中的应用

应用场景1:运动命令队列

场景描述: UI 线程接收用户操作(如点击"移动"按钮),生成运动命令,但这些命令需要由运动控制线程按顺序执行。

为什么需要队列:

  1. 用户可能快速连续点击多个按钮

  2. 运动控制卡的执行速度有限

  3. 需要保证命令按顺序执行

  4. 避免阻塞 UI 线程

实现示例:

cs 复制代码
// 定义运动命令类型
public class MotionCommand
{
    public int AxisId { get; set; }
    public double TargetPosition { get; set; }
    public double Velocity { get; set; }
    public CommandType Type { get; set; }
}

public enum CommandType
{
    Move,
    Home,
    Stop,
    Jog
}

// 运动控制器类
public class MotionController
{
    private readonly Channel<MotionCommand> _commandChannel;
    private readonly CancellationTokenSource _cancellationTokenSource;
    private Task _consumerTask;

    public MotionController()
    {
        // 创建有界命令队列,最多100个命令
        _commandChannel = Channel.CreateBounded<MotionCommand>(
            new BoundedChannelOptions(100)
            {
                FullMode = BoundedChannelFullMode.Wait
            });
        
        _cancellationTokenSource = new CancellationTokenSource();
    }

    // 启动消费者线程
    public void Start()
    {
        _consumerTask = Task.Run(async () =>
        {
            await foreach (var command in _commandChannel.Reader.ReadAllAsync(
                _cancellationTokenSource.Token))
            {
                try
                {
                    await ExecuteCommand(command);
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"Command execution failed: {ex.Message}");
                }
            }
        });
    }

    // 停止控制器
    public async Task StopAsync()
    {
        _cancellationTokenSource.Cancel();
        _commandChannel.Writer.Complete();
        
        if (_consumerTask != null)
        {
            await _consumerTask;
        }
    }

    // 添加运动命令(生产者)
    public async Task AddCommandAsync(MotionCommand command)
    {
        await _commandChannel.Writer.WriteAsync(command);
    }

    // 执行命令(消费者)
    private async Task ExecuteCommand(MotionCommand command)
    {
        Console.WriteLine($"Executing command: {command.Type} on axis {command.AxisId}");
        
        switch (command.Type)
        {
            case CommandType.Move:
                // 调用运动控制卡 API 执行移动
                await ExecuteMove(command.AxisId, command.TargetPosition, command.Velocity);
                break;
                
            case CommandType.Home:
                await ExecuteHome(command.AxisId);
                break;
                
            case CommandType.Stop:
                await ExecuteStop(command.AxisId);
                break;
                
            case CommandType.Jog:
                await ExecuteJog(command.AxisId, command.Velocity);
                break;
        }
    }

    // 模拟运动控制卡 API 调用
    private async Task ExecuteMove(int axisId, double position, double velocity)
    {
        // 这里调用实际的 SDK API
        Console.WriteLine($"Axis {axisId}: Moving to {position} at {velocity}");
        await Task.Delay(100); // 模拟执行时间
    }

    private async Task ExecuteHome(int axisId)
    {
        Console.WriteLine($"Axis {axisId}: Homing...");
        await Task.Delay(500);
    }

    private async Task ExecuteStop(int axisId)
    {
        Console.WriteLine($"Axis {axisId}: Stopping");
        await Task.Delay(50);
    }

    private async Task ExecuteJog(int axisId, double velocity)
    {
        Console.WriteLine($"Axis {axisId}: Jogging at {velocity}");
        await Task.Delay(100);
    }
}

UI 线程使用示例:

cs 复制代码
// 在 WinForms 或 WPF 中使用
public class MainForm : Form
{
    private readonly MotionController _motionController;

    public MainForm()
    {
        _motionController = new MotionController();
        _motionController.Start();
    }

    private async void btnMove_Click(object sender, EventArgs e)
    {
        var command = new MotionCommand
        {
            AxisId = 1,
            TargetPosition = 1000,
            Velocity = 500,
            Type = CommandType.Move
        };

        await _motionController.AddCommandAsync(command);
    }

    private async void btnHome_Click(object sender, EventArgs e)
    {
        var command = new MotionCommand
        {
            AxisId = 1,
            Type = CommandType.Home
        };

        await _motionController.AddCommandAsync(command);
    }

    protected override void OnFormClosing(FormClosingEventArgs e)
    {
        _motionController.StopAsync().Wait();
        base.OnFormClosing(e);
    }
}

应用场景2:数据采集与处理

场景描述: 定时器定时采集传感器数据,采集的数据需要实时处理和显示,但处理速度可能慢于采集速度。

实现示例:

cs 复制代码
public class SensorData
{
    public int SensorId { get; set; }
    public double Value { get; set; }
    public DateTime Timestamp { get; set; }
}

public class DataProcessor
{
    private readonly Channel<SensorData> _dataChannel;
    private readonly CancellationTokenSource _cancellationTokenSource;

    public DataProcessor()
    {
        // 使用无界通道,确保不丢失数据
        _dataChannel = Channel.CreateUnbounded<SensorData>();
        _cancellationTokenSource = new CancellationTokenSource();
    }

    // 添加传感器数据(生产者)
    public void AddData(SensorData data)
    {
        // 使用 TryWrite,如果失败则丢弃旧数据
        if (!_dataChannel.Writer.TryWrite(data))
        {
            Console.WriteLine("Warning: Failed to add data to channel");
        }
    }

    // 启动处理线程(消费者)
    public void StartProcessing()
    {
        Task.Run(async () =>
        {
            await foreach (var data in _dataChannel.Reader.ReadAllAsync(
                _cancellationTokenSource.Token))
            {
                ProcessData(data);
            }
        });
    }

    // 处理数据
    private void ProcessData(SensorData data)
    {
        // 数据过滤
        if (data.Value < 0 || data.Value > 100)
        {
            return;
        }

        // 数据计算
        var processedValue = data.Value * 1.5;

        // 更新显示(需要切换到 UI 线程)
        UpdateDisplay(data.SensorId, processedValue);

        // 保存到数据库
        SaveToDatabase(data);

        Console.WriteLine($"Processed sensor {data.SensorId}: {data.Value} -> {processedValue}");
    }

    private void UpdateDisplay(int sensorId, double value)
    {
        // 在 WinForms 中使用 Invoke
        // this.Invoke((MethodInvoker)delegate {
        //     lblSensorValue.Text = value.ToString("F2");
        // });
    }

    private void SaveToDatabase(SensorData data)
    {
        // 保存到数据库
    }

    public void Stop()
    {
        _cancellationTokenSource.Cancel();
        _dataChannel.Writer.Complete();
    }
}

定时器采集示例:

cs 复制代码
public class DataCollector
{
    private readonly DataProcessor _processor;
    private readonly Timer _timer;

    public DataCollector(DataProcessor processor)
    {
        _processor = processor;
        
        // 每 10ms 采集一次数据
        _timer = new Timer(100);
        _timer.Elapsed += OnTimerElapsed;
    }

    public void Start()
    {
        _timer.Start();
    }

    public void Stop()
    {
        _timer.Stop();
    }

    private void OnTimerElapsed(object sender, ElapsedEventArgs e)
    {
        // 模拟读取传感器数据
        var data = new SensorData
        {
            SensorId = 1,
            Value = ReadSensorValue(),
            Timestamp = DateTime.Now
        };

        // 添加到处理队列
        _processor.AddData(data);
    }

    private double ReadSensorValue()
    {
        // 模拟读取传感器
        return new Random().NextDouble() * 100;
    }
}

总结

核心要点

  1. 生产者-消费者模式是一种重要的多线程设计模式,用于解耦数据生产和处理

  2. Channel<T> 是 .NET 中推荐的生产者-消费者集合,高性能且易于使用

  3. BlockingCollection<T> 是传统选择,适用于 .NET Framework 或需要更多控制的场景

  4. 在运动控制中,此模式广泛应用于命令队列、数据采集、日志记录等场景

  5. 最佳实践包括:合理选择容量、使用取消令牌、正确处理异常、批处理优化等

相关推荐
电商API&Tina2 小时前
乐天平台 (Rakuten) 数据采集指南
大数据·开发语言·数据库·oracle·json
今晚打老虎z2 小时前
解决SQL Server 安装运行时针对宿主机内存不足2GB的场景
sqlserver·c#
zhougl9962 小时前
Java内部类详解
java·开发语言
Grassto2 小时前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
代码游侠2 小时前
学习笔记——Linux内核与嵌入式开发3
开发语言·arm开发·c++·学习
怎么没有名字注册了啊2 小时前
C++ 进制转换
开发语言·c++
代码游侠2 小时前
C语言核心概念复习(二)
c语言·开发语言·数据结构·笔记·学习·算法
冰暮流星2 小时前
javascript之双重循环
开发语言·前端·javascript
墨月白2 小时前
[QT]QProcess的相关使用
android·开发语言·qt