4、队列进阶:队列集、队列锁、多队列优先级设计、队列实战-命令解析
队列这东西,基础用法大家都会。但真正到了项目里,你会发现——一个队列根本不够用。
我记得第一次做多传感器数据采集的项目,三个传感器,每个都往主控发数据。我一开始图省事,全塞进一个队列里。结果呢?数据全混在一起,根本分不清哪个是温度、哪个是湿度、哪个是气压。后来我学乖了,开始用队列集和队列锁。今天咱们就把这些进阶玩法聊透。
4.1 队列集——把多个队列管起来
队列集是什么?说白了就是一个“队列的队列”。你创建好几个队列,然后把它们注册到一个队列集里。任务在等待时,只要队列集中任何一个队列有数据,它就会被唤醒。
我习惯把队列集想象成一个前台接待员。多个队列就像多个窗口,哪个窗口有人喊号,接待员就告诉你“去3号窗口”。你不用挨个窗口去问。
核心API:
xQueueCreateSet()— 创建队列集xQueueAddToSet()— 把队列加入集合xQueueSelectFromSet()— 从集合中获取有数据的队列句柄
来看个实际例子。假设你有两个队列:一个接收按键事件,一个接收网络数据包。
// 创建两个队列
QueueHandle_t keyQueue = xQueueCreate(10, sizeof(uint8_t));
QueueHandle_t netQueue = xQueueCreate(5, sizeof(char*));
// 创建队列集
QueueSetHandle_t queueSet = xQueueCreateSet(10 + 5);
// 把队列注册进去
xQueueAddToSet(keyQueue, queueSet);
xQueueAddToSet(netQueue, queueSet);
// 任务中等待
void vProcessTask(void *pvParameters) {
QueueSetMemberHandle_t activeQueue;
uint8_t keyVal;
char *netData;
for (;;) {
// 阻塞等待任意队列有数据
activeQueue = xQueueSelectFromSet(queueSet, portMAX_DELAY);
if (activeQueue == keyQueue) {
xQueueReceive(keyQueue, &keyVal, 0);
// 处理按键
} else if (activeQueue == netQueue) {
xQueueReceive(netQueue, &netData, 0);
// 处理网络数据
}
}
}
我的经验:队列集的大小要等于所有子队列大小之和。少了会出问题,多了浪费内存。我一般会多留10%的余量。
4.2 队列锁——别让数据乱掉
队列本身是线程安全的,这没错。但如果你在操作队列前后,还需要保护一些共享资源呢?比如一个全局变量,或者一个外设寄存器。
这时候就需要队列锁。其实FreeRTOS没有专门的“队列锁”API,我们通常用信号量或者互斥量来实现。我个人习惯用互斥量,因为它自带优先级继承,能避免优先级反转。
// 定义一个互斥量保护队列操作
SemaphoreHandle_t queueLock;
void vSenderTask(void *pvParameters) {
int32_t data = 100;
for (;;) {
// 获取锁
xSemaphoreTake(queueLock, portMAX_DELAY);
// 临界区:操作队列 + 共享资源
xQueueSend(dataQueue, &data, 0);
sharedCounter++; // 这个全局变量也需要保护
// 释放锁
xSemaphoreGive(queueLock);
vTaskDelay(pdMS_TO_TICKS(100));
}
}
注意:千万不要在持有锁的时候调用阻塞API,比如 vTaskDelay() 或 xQueueReceive() 带超时。我曾经在一个项目里这么干过,结果其他任务全被卡死了。嗯,血的教训。
4.3 多队列优先级设计
实际项目中,不同数据的重要程度是不一样的。比如紧急停止命令必须立刻处理,而日志数据可以慢慢来。
我常用的做法是:给不同优先级的队列分配不同的队列,然后在接收任务里按优先级顺序检查。
// 定义三个优先级队列
QueueHandle_t highPriQueue; // 紧急命令
QueueHandle_t midPriQueue; // 常规数据
QueueHandle_t lowPriQueue; // 日志/调试
void vPriorityReceiver(void *pvParameters) {
uint32_t cmd;
for (;;) {
// 先检查高优先级
if (xQueueReceive(highPriQueue, &cmd, 0) == pdTRUE) {
processHighPriority(cmd);
continue; // 处理完继续检查,防止低优先级任务饿死高优先级
}
// 再检查中优先级
if (xQueueReceive(midPriQueue, &cmd, 0) == pdTRUE) {
processMidPriority(cmd);
continue;
}
// 最后检查低优先级
if (xQueueReceive(lowPriQueue, &cmd, 0) == pdTRUE) {
processLowPriority(cmd);
continue;
}
// 所有队列都空,才阻塞等待
vTaskDelay(pdMS_TO_TICKS(10));
}
}
你想想看,这种设计的好处是什么?高优先级命令永远不会被低优先级数据堵住。哪怕低优先级队列里塞了一万条日志,紧急命令一来,立刻就能处理。
设计原则:
- 高优先级队列用
pdMS_TO_TICKS(0)轮询,不阻塞 - 低优先级队列用
portMAX_DELAY阻塞,省CPU - 每个队列大小要合理估算,防止高优先级队列溢出
4.4 队列实战——命令解析器
好了,理论讲完了,咱们来点实战。下面是一个简单的命令解析器,它接收串口发来的字符串命令,解析后分发到不同处理函数。
// 命令队列
QueueHandle_t cmdQueue;
// 命令结构体
typedef struct {
uint8_t cmdId;
uint8_t paramLen;
uint8_t params[16];
} Command_t;
// 串口接收任务
void vUartRxTask(void *pvParameters) {
Command_t cmd;
uint8_t buffer[32];
uint16_t len;
for (;;) {
// 从串口接收数据(伪代码)
len = uart_receive(buffer, sizeof(buffer), portMAX_DELAY);
// 解析命令
if (parseCommand(buffer, len, &cmd)) {
// 发送到命令队列
if (xQueueSend(cmdQueue, &cmd, pdMS_TO_TICKS(100)) != pdTRUE) {
// 队列满了,记录错误
error_log("Command queue full!");
}
}
}
}
// 命令处理任务
void vCmdProcessTask(void *pvParameters) {
Command_t cmd;
for (;;) {
if (xQueueReceive(cmdQueue, &cmd, portMAX_DELAY) == pdTRUE) {
switch (cmd.cmdId) {
case CMD_MOTOR_START:
motor_start(cmd.params);
break;
case CMD_MOTOR_STOP:
motor_stop();
break;
case CMD_SET_SPEED:
motor_set_speed(*(int32_t*)cmd.params);
break;
default:
error_log("Unknown command: %d", cmd.cmdId);
break;
}
}
}
}
避坑指南:我曾经在命令解析器里犯过一个低级错误——命令队列大小设成了1。结果串口连续发两条命令时,第二条直接丢了。后来我改成队列大小等于命令缓冲区深度,问题就解决了。记住,队列大小要能容纳峰值流量。
4.5 本章知识体系
下面这张图帮你梳理一下队列进阶的核心逻辑:
队列集帮你管理多个队列,队列锁保护共享资源,多队列优先级确保紧急任务不被耽误,命令解析器则是把这些技术串起来的实战案例。嗯,掌握了这些,你写出来的系统会稳很多。
公众号:蓝海资料掘金营,微信deep3321