模式概述
什么是生产者-消费者模式?
生产者-消费者模式(Producer-Consumer Pattern)是一种经典的多线程设计模式,它描述了如何协作地处理数据流。想象一个现实生活中的场景:
生活中的例子:餐厅后厨
厨师(生产者) 传菜窗口(缓冲区) 服务员(消费者)
↓ ↓ ↓
做菜 放置菜品 端给客人
↓ ↓ ↓
持续制作 临时存放 持续服务
在这个场景中:
-
厨师:负责制作菜品,不关心谁端菜,也不关心客人吃多快
-
传菜窗口:临时存放菜品的地方,容量有限
-
服务员:负责把菜端给客人,不关心菜怎么做的
如果厨师做得太快,窗口满了,厨师就得等一下(阻塞或丢弃) 如果服务员端得快,窗口空了,服务员就得等一下(阻塞)
为什么需要这个模式?
在软件开发中,生产者-消费者模式解决了以下问题:
-
解耦生产者和消费者:生产者不需要知道消费者的存在,反之亦然
-
平衡处理速度:当生产速度和消费速度不一致时,缓冲区可以起到缓冲作用
-
提高并发性:生产者和消费者可以并行工作
-
简化错误处理:某个环节出错不会影响其他环节
在运动控制中的重要性
运动控制系统中,生产者-消费者模式非常重要:
上位机指令(生产者) 命令队列(缓冲区) 控制卡执行(消费者)
↓ ↓ ↓
发送运动指令 临时存储 执行运动
↓ ↓ ↓
可能快速发送 平滑处理 按实际速度执行
核心概念
三个核心组件
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>?
-
专为异步设计:所有操作都是异步的,不会阻塞线程
-
高性能:内部使用高效的数据结构和同步机制
-
灵活的容量控制:支持有界和无界通道
-
优雅的完成机制:支持优雅地通知通道关闭
-
官方推荐:微软官方文档推荐的生产者-消费者方案
基本用法
创建 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> 仍然有用:
-
许多现有代码使用此方案
-
.NET Framework 项目必须使用它
-
提供一些 Channel<T> 没有的功能(如取消令牌)
-
同步场景可能更适合
基本用法
创建 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 线程接收用户操作(如点击"移动"按钮),生成运动命令,但这些命令需要由运动控制线程按顺序执行。
为什么需要队列:
-
用户可能快速连续点击多个按钮
-
运动控制卡的执行速度有限
-
需要保证命令按顺序执行
-
避免阻塞 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;
}
}
总结
核心要点
-
生产者-消费者模式是一种重要的多线程设计模式,用于解耦数据生产和处理
-
Channel<T> 是 .NET 中推荐的生产者-消费者集合,高性能且易于使用
-
BlockingCollection<T> 是传统选择,适用于 .NET Framework 或需要更多控制的场景
-
在运动控制中,此模式广泛应用于命令队列、数据采集、日志记录等场景
-
最佳实践包括:合理选择容量、使用取消令牌、正确处理异常、批处理优化等