RT-Thread多线程通信机制实战:队列 vs 信号量 vs 事件
在RT-Thread(RTT)嵌入式实时系统中,多线程间的同步与通信是应用稳定性的基石。信号量(Semaphore)、队列(Message Queue)与事件集(Event)分别对应“通知”“传数”“多条件等待”三种典型场景。本文将通过STM32上的实战代码,帮你快速掌握三者的选用原则与避坑要点。
一、信号量:资源计数与二值同步
信号量适合“我干完了,你可以开始”的单向同步,或“N个同类资源可用吗”的资源计数。
1. 二值信号量(Binary Semaphore)—— 中断→线程通知
最经典用法:按键/定时器ISR告知处理线程“有新事件”。
#include <rtthread.h>
#define THREAD_STACK 512
#define THREAD_PRIO 20
static rt_sem_t sem_btn;
/* 消费线程 */
static void btn_thread_entry(void *param)
{
while (1)
{
/* 阻塞等待信号量(中断中释放) */
rt_sem_take(sem_btn, RT_WAITING_FOREVER);
rt_kprintf("Button pressed, do action!\n");
}
}
/* 模拟ISR(通常在外部中断回调中调用) */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
{
rt_sem_release(sem_btn); // 不能在ISR中做I/O,仅释放信号量
}
}
int btn_sample_init(void)
{
sem_btn = rt_sem_create("btn_sem", 0, RT_IPC_FLAG_FIFO);
if (!sem_btn) return -1;
rt_thread_t tid = rt_thread_create("btn", btn_thread_entry,
RT_NULL, THREAD_STACK,
THREAD_PRIO, 20);
if (tid) rt_thread_startup(tid);
return 0;
}
INIT_APP_EXPORT(btn_sample_init);
要点:
• 初值0=二值(等待方先阻塞);
• RT_IPC_FLAG_FIFO/PRIO决定等待线程唤醒顺序;
• ISR中严禁rt_sem_take(),只能rt_sem_release()。
2. 计数信号量——管理有限资源池
rt_sem_t sem_pool = rt_sem_create("pool", 3, RT_IPC_FLAG_FIFO); // 3个资源
/* 线程取资源 */
rt_sem_take(sem_pool, RT_WAITING_FOREVER);
/* 用完归还 */
rt_sem_release(sem_pool);
二、队列(Message Queue):带数据的线程通信
当线程间需传递结构化数据(而非简单“告知”)时,队列是唯一正确选择。
#define MQ_MAX_MSGS 10
#define MQ_MSG_SIZE sizeof(struct sensor_data)
struct sensor_data
{
uint32_t timestamp;
int16_t temp;
int16_t humi;
};
static rt_mq_t mq_sensor;
/* 采集线程 → 发送数据 */
static void sensor_collect_entry(void *param)
{
struct sensor_data pkt;
pkt.timestamp = rt_tick_get();
pkt.temp = 256;
pkt.humi = 600;
/* 若队列满可择性丢弃或等待 */
rt_mq_send(mq_sensor, &pkt, MQ_MSG_SIZE);
}
/* 处理线程 → 接收数据 */
static void sensor_proc_entry(void *param)
{
struct sensor_data rcv;
while (1)
{
if (rt_mq_recv(mq_sensor, &rcv, MQ_MSG_SIZE,
rt_tick_from_millisecond(500)) == RT_EOK)
{
rt_kprintf("T=%d, H=%d\n", rcv.temp/10, rcv.humi/10);
}
}
}
int mq_sample_init(void)
{
mq_sensor = rt_mq_create("sen_mq", MQ_MSG_SIZE,
MQ_MAX_MSGS, RT_IPC_FLAG_FIFO);
if (!mq_sensor) return -1;
/* 创建采集/处理线程(略,同信号量例) */
return 0;
}
INIT_APP_EXPORT(mq_sample_init);
对比信号量优势:队列自带缓冲区,发送方不会被接收方节奏阻塞(只要不满),且数据有序、线程安全。
三、事件集(Event):多标志位“与/或”等待
事件集适合“多个条件任意一个/全部满足才继续”的场景,如初始化流程需等“Flash就绪 AND 网络链路 UP”。
#define EV_FLASH_RDY (1 << 0)
#define EV_NET_LINK (1 << 1)
static rt_event_t ev_sys;
/* 初始化线程 */
int event_sample_init(void)
{
ev_sys = rt_event_create("sys_ev", RT_IPC_FLAG_FIFO);
return ev_sys ? 0 : -1;
}
INIT_APP_EXPORT(event_sample_init);
/* Flash驱动初始化完成 → 置位 */
void flash_ready_cb(void)
{
rt_event_send(ev_sys, EV_FLASH_RDY);
}
/* Net链路UP → 置位 */
void net_link_up_cb(void)
{
rt_event_send(ev_sys, EV_NET_LINK);
}
/* 主任务:等两者都就绪(AND逻辑) */
static void main_task_entry(void *param)
{
rt_uint32_t recv_set = 0;
rt_event_recv(ev_sys,
EV_FLASH_RDY | EV_NET_LINK,
RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR,
RT_WAITING_FOREVER,
&recv_set);
rt_kprintf("System fully initialized!\n");
}
• RT_EVENT_FLAG_AND → 所有位均置位才唤醒
• RT_EVENT_FLAG_OR → 任一位置位即唤醒
• 建议配合RT_EVENT_FLAG_CLEAR避免重复触发
⚠️ 事件不携带数据,仅传递位图,不能替代队列传结构体。
四、三者选型速查表
需求 推荐IPC 原因
中断通知线程“有事做” 二值信号量 快、无数据拷贝,ISR安全
线程A → 线程B 传结构体 队列 带缓冲、保序、类型安全
等“条件A 且/或 条件B” 事件集 支持多标志位组合判断
管理N个相同外设(如DMA缓冲) 计数信号量 直观反映资源余量
需记录历史数据供慢线程处理 队列 信号量无缓存能力
五、常见坑与调试技巧
1. 在ISR中调错API:ISR只能用rt_sem_release()、rt_mq_urgent()、rt_event_send(),禁止rt_xxx_take(),否则触发scheduler in ISR断言失败。
2. 队列满丢数:高吞吐建议适当加大MQ_MAX_MSGS,或在发送前rt_mq_get_msg_num()判断,满时做标记丢弃而非死等。
3. 事件位不清除导致死循环:若使用循环rt_event_recv(..., OR, CLEAR),确保消费逻辑正确处理所有置位条件,否则可能反复唤醒。
4. 优先级反转:默认二值信号量无优先级继承,若高优先级线程等待低优先级持有的信号量,建议配合互斥锁(rt_mutex_t)保护临界资源,信号量仅用于同步。
六、结语
RT-Thread的多线程通信三件套各司其职:信号量同步、队列传数、事件集等多条件等待。在STM32等资源受限平台,按上表选型并严格遵守ISR API限制,即可构建出既高效又易维护的实时系统。





