文章目录
前言
--
一、什么是队列?什么是阻塞队列?
- 队列(Queue)就是排队就跟你在超市结账、车站买票一样,先来的先走后来的后走,这种先进先出(FIFO, First In First Out)的数据结构,就叫队列。
- 阻塞队列(BlockingCollection)简单表述就是需要等待阻塞的队列。队列为空时,取数据线程会自动阻塞休眠,直到有数据才唤醒;队列有上限时,存数据线程会自动阻塞休眠,直到有空位才唤醒,这种会自动等待、不报错、不卡死的队列,就叫阻塞队列。
- 我们在通信和数据处理中经常用到队列,本文就C#中的队列进行简单描述。C#中的类是叫Queue。阻塞队列在C#中的类叫BlockingCollection。
二、普通和阻塞队列对比
| 特性 | Queue 普通队列 | BlockingCollection 阻塞队列 |
|---|---|---|
| 线程安全 | 非安全(多线程必报错) | 完全线程安全 |
| 阻塞能力 | 无(空 / 满直接抛异常) | 有(空时取阻塞,满时存阻塞) |
| 并发支持 | 不支持多线程并发 | 原生支持多线程并发 |
| 边界限制 | 无上限(无限扩容) | 可设置最大容量(有界队列) |
| 完成标记 | 不支持 | 支持 CompleteAdding() 标记生产完成 |
| 性能 | 单线程极快 | 多线程下稳定,有轻微锁开销 |
| 适用场景 | 单线程业务、简单数据缓存 | 多线程、异步通信、任务队列、生产者消费者 |
- 从上表可以寄看出 普通队列的优势无锁、无阻塞逻辑,单线程下速度最快轻量简单内存占用小。比较适用单线程处理数据(如顺序缓存、临时存储)无并发、无异步的简单业务。
- 而阻塞队列的线程安全方面有着明显的优势,多线程并发读写不会崩溃、不会数据错乱。自动阻塞、唤醒和优雅退出,可防止过快导致内存崩溃等,更加适用于多线程任务调度。
三、代码描述
- 代码将普通队列和阻塞队列一起描述 可以通过宏定义开关(SELECT_QUEUE)切换,任君自主选择。无论是使用普通队列还是阻塞队列都在一定数据量内都是可以正常运行的。
- 实例中除了主线程,还启动了2个独立的线程来分别处理接收数据和数据解析。
1、新建队列
- 新建初始化队列没有书名特殊,new一个即可,要注意using引用相关的库
c
//#define SELECT_QUEUE //宏定义开关
using System.Collections;
using System.Collections.Concurrent;
using System.Threading;
//新建队列
#if SELECT_QUEUE
//新建一个队列接收数据
public Queue q_rx = new Queue();
#else
//新建阻塞队列BlockingCollection
public BlockingCollection<byte[]> q_rx = new BlockingCollection<byte[]>();
#endif
2、入队列
- 在线程_receiveThread中获取的数据直接入队列,他们的方法不一样,普通队列为Enqueue()方法,阻塞队列入队则是Add()方法。
c
Thread _receiveThread;//接收线程
public bool rx_thread_stop_flag = false;//线程停止标志
//接收到的数据, 数据长度不确定
public byte[]RxBuf;
/// <summary>
/// 开关接收线程
/// </summary>
private void RxThreadOnOff(bool on_off)
{
if (on_off)
{
_receiveThread = new Thread(() => ReceiveData());//开启一个线程来不断的接收数据
_receiveThread.IsBackground = true;
_receiveThread.Name = "UartReceiveDataThread";
_receiveThread.Start();
Console.Write($"UartReceiveDataThread.ID:{_receiveThread.ManagedThreadId}\n");
}
else
{
rx_thread_stop_flag = true;
while (_receiveThread!=null)//等待停止
{
_receiveThread.Join(300); // 等待线程结束
_receiveThread = null;
}
rx_thread_stop_flag = false;
}
}
/// <summary>
/// 接收数据处理入队
/// </summary>
public void ReceiveData()
{
//接收数据线程循环
while ( !rx_thread_stop_flag)//线程循环
{
//间隔20ms
Thread.Sleep(20)
//获取实时的数据
byte[] data=ReadData();
//数据长度
int len = data.Length;
RxBuf = new byte[len];
Array.Copy(data, RxBuf, len);//将接收到的数据复制到全局变量RxBuf中
#if SELECT_QUEUE
q_rx.Enqueue(RxBuf);//数据入到普通队列
#else
q_rx.Add(RxBuf);//数据入到阻塞队列
#endif
}
}
3、出队列
- 从下面代码中可以看到,RxParsingThread线程中出队列时,普通队列使用Dequeue()方法出队,而阻塞队列则使用Take()方法等待和出队列。出队后通过BufferReceiveParsing()中自定义解析或后续动作
代码如下(示例):
c
//接收解析线程
Thread RxParsingThread;
/// <summary>
/// 启动解析线程
/// </summary>
private void RxParsingThreadStart()
{
RxParsingThread = new Thread(new ThreadStart(BufferReceiveParsingLoop))//指定线程函数
{
IsBackground = true,//可后台运行
Name = "RxParsingThread"//线程名
};
RxParsingThread.Start();//启动线程
Console.Write($"RxParsingThreadStart.ID:{RxParsingThread.ManagedThreadId}\n");
}
/// <summary>
/// 接收解析循环
/// </summary>
public void BufferReceiveParsingLoop()
{
while (true)
{
#if SELECT_QUEUE
//判断队列是否有值
if (q_rx.Count > 0 )
{
//数据出队列
byte[] rxData = (byte[])q_rx.Dequeue();
BufferReceiveParsing(rxData);//解析数据
}
else
{
Thread.Sleep(1);
}
#else
//阻塞队列出队
if (!q_rx.IsCompleted)
{
//数据出队列,为空阻塞在此
byte[] rxData = q_rx.Take(); // 阻塞等待数据
BufferReceiveParsing(rxData);//解析数据
}
#endif
}
}
//自定义数据解析
public void BufferReceiveParsing(byte[] rxData)
{}
总结
- 本文描述了C# 中常用的两种队列,并进行了对比和较为详细的代码描述。
- 单线程场景,用普通队列Queue,轻量、速度快。
- 多线程 / 多并发场景,强烈推荐用阻塞队列BlockingCollection,安全、自动阻塞、代码同样简洁。
- 线程安全 + 阻塞等待,这是阻塞队列最核心的价值。