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 本章知识体系

下面这张图帮你梳理一下队列进阶的核心逻辑:

队列进阶知识体系 队列进阶 队列集 管理多个队列 xQueueCreateSet 队列锁 保护共享资源 互斥量实现 多队列优先级 高优先处理 轮询+阻塞 命令解析实战 串口接收→解析→分发 队列大小估算 四个核心知识点,层层递进

队列集帮你管理多个队列,队列锁保护共享资源,多队列优先级确保紧急任务不被耽误,命令解析器则是把这些技术串起来的实战案例。嗯,掌握了这些,你写出来的系统会稳很多。


公众号:蓝海资料掘金营,微信deep3321