尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

RT-Thread线程同步与通信:信号量、互斥量、事件集、消息队列实战指南

RT-Thread线程同步与通信:信号量、互斥量、事件集、消息队列实战指南 1. 从“单打独斗”到“协同作战”为什么需要线程同步与通信在嵌入式开发里尤其是跑RTOS实时操作系统的项目你写的代码很少是“一条道走到黑”的。想象一下你有一个智能家居的温控器它需要同时做几件事一个任务线程负责从传感器读取温度一个任务负责计算是否需要启动加热或制冷还有一个任务负责刷新液晶屏显示当前状态和设定值。如果这三个任务各干各的互不搭理那会出什么乱子最经典的场景就是显示任务正要把“25°C”这个数字刷到屏幕上刚写了一半计算任务突然更新了温度值为“26°C”。结果屏幕上可能显示出一半是“25”一半是“6”的乱码或者直接显示了一个完全错误的数值。这就是典型的“资源竞争”问题多个线程在没有协调的情况下同时访问读或写同一个共享资源比如存储温度值的全局变量导致了数据不一致、逻辑错乱甚至系统崩溃。所以线程同步要解决的核心问题就是“有序访问”。它确保在某一时刻共享资源只被一个线程安全地使用其他线程需要等待。这就像十字路口的红绿灯或者银行柜台的叫号机防止大家一拥而上造成混乱。而线程通信要解决的则是“信息传递”和“工作协同”。光有序还不够线程之间还得能“说话”。比如传感器读取线程在获取到新的温度值后需要“通知”计算线程“嘿新数据来了你可以干活了”计算线程处理完后又需要“通知”显示线程“结果算好了可以更新屏幕了。”如果没有这种通信机制线程就只能傻傻地轮询不断检查某个标志位这既浪费宝贵的CPU时间又增加了系统功耗在电池供电的设备里是绝对要避免的。在RT-Thread这类实时操作系统中内核提供了丰富的机制来帮你搞定这两大难题。理解并熟练运用它们是从“能跑”的代码到“稳定、高效”的产品的关键一步。接下来我们就深入看看RT-Thread给我们准备了哪些“武器”。2. RT-Thread的同步“武器库”信号量、互斥量与事件集RT-Thread提供了几种主流的同步原语每种都有其特定的适用场景和细微差别。选对了工具事半功倍用错了可能会引入死锁或性能瓶颈。2.1 信号量最通用的“资源计数器”与“任务触发器”你可以把信号量想象成一个存放“令牌”的盒子。初始化时你可以决定盒子里放多少个令牌。线程要访问共享资源必须先从这个盒子里获取一个令牌。如果盒子空了信号量值为0线程就得排队等待直到有其他线程释放一个令牌回来。核心操作rt_sem_init()/rt_sem_create(): 初始化或动态创建一个信号量并设定初始值。rt_sem_take(): 获取信号量。如果信号量值 0则值减1线程继续执行如果值等于0则线程挂起等待。rt_sem_release(): 释放信号量。信号量值加1如果有线程在等待则唤醒其中一个。两种经典用法二值信号量初始值为1用于互斥访问。这相当于盒子里只有1个令牌任何时刻只有一个线程能持有它从而保护临界区。虽然互斥量更适合这个场景但信号量也能实现。/* 全局信号量用于保护共享变量 shared_data */ static rt_sem_t data_sem; /* 初始化信号量初始有1个令牌 */ data_sem rt_sem_create(dataSem, 1, RT_IPC_FLAG_FIFO); void thread1_entry(void *parameter) { while (1) { /* 进入临界区前获取令牌 */ rt_sem_take(data_sem, RT_WAITING_FOREVER); /* 安全地操作 shared_data */ shared_data; /* 离开临界区归还令牌 */ rt_sem_release(data_sem); rt_thread_delay(100); } }注意虽然可以但不推荐用二值信号量做互斥。因为它没有“所有权”概念。任何线程都能释放rt_sem_release一个信号量即使它并没有获取rt_sem_take过。这可能导致逻辑错误。互斥量是更好的选择。计数信号量初始值为N用于管理一组数量有限的资源。比如你有5个串口缓冲区可供使用。初始化一个计数为5的信号量。线程需要使用缓冲区时take一个信号量计数减1用完释放时release一个信号量计数加1。当计数为0时申请缓冲区的线程需要等待。同步信号量初始值为0用于线程间同步这是信号量最常用也最优雅的用法之一。线程A等待某个事件发生线程B在事件发生后释放信号量。/* 初始化信号量为0表示“事件尚未发生” */ rt_sem_t event_sem rt_sem_create(eventSem, 0, RT_IPC_FLAG_FIFO); /* 线程B数据采集线程 */ void sensor_thread_entry(void *parameter) { while (1) { /* 模拟读取传感器数据 */ read_sensor_data(); /* 数据就绪释放信号量通知处理线程 */ rt_sem_release(event_sem); rt_thread_delay(500); // 每500ms采集一次 } } /* 线程A数据处理线程 */ void process_thread_entry(void *parameter) { while (1) { /* 等待数据就绪的信号。如果没有数据线程在此挂起不消耗CPU */ rt_sem_take(event_sem, RT_WAITING_FOREVER); /* 收到信号说明数据已就绪开始处理 */ process_data(); } }这种方式彻底消除了轮询处理线程只在有实际工作时才被调度极大地提高了系统效率。2.2 互斥量专为互斥访问而生的“锁”互斥量是信号量在互斥访问场景下的“特化升级版”。它同样只包含0/1两个状态但关键区别在于所有权。核心特性所有权只有成功获取rt_mutex_take了互斥量的线程才能释放rt_mutex_release它。这防止了其他线程错误释放锁。优先级继承这是互斥量最重要的特性也是它优于二值信号量的根本原因。假设高优先级线程H等待低优先级线程L持有的互斥量。如果没有优先级继承H会一直等待L而L可能被中优先级线程M抢占导致H无限期等待优先级反转。RT-Thread的互斥量具有优先级继承机制当H等待L持有的锁时系统会临时将L的优先级提升到与H相同让L尽快执行完临界区代码并释放锁从而让H能尽快运行。释放锁后L的优先级恢复原样。递归锁同一个线程可以多次获取同一个互斥量而不会死锁但释放次数必须与获取次数相同。核心操作rt_mutex_init()/rt_mutex_create()rt_mutex_take()rt_mutex_release()使用场景任何需要保护共享资源全局变量、链表、外设寄存器的临界区都应优先使用互斥量而不是二值信号量。static rt_mutex_t uart_tx_mutex; // 用于保护串口发送函数防止多线程同时调用造成数据交错 void uart_send_string(const char *str) { rt_mutex_take(uart_tx_mutex, RT_WAITING_FOREVER); /* 临界区向串口发送数据这个函数可能不是线程安全的 */ send_data_via_uart(str); rt_mutex_release(uart_tx_mutex); }2.3 事件集等待多个事件的“多路开关”事件集允许一个线程等待多个事件中的任意一个或全部发生。每个事件用一个特定的位bit来表示。核心操作rt_event_init()/rt_event_create(): 创建事件集。rt_event_send(): 发送事件设置事件位。rt_event_recv(): 接收事件。可以设置等待模式RT_EVENT_FLAG_AND: 逻辑与等待所有指定事件都发生。RT_EVENT_FLAG_OR: 逻辑或等待任意一个指定事件发生。RT_EVENT_FLAG_CLEAR: 在接收成功后自动清除已收到的事件标志。使用场景线程需要等待来自多个其他线程或中断的多种不同类型的事件。比如一个网络处理线程可能需要同时等待“收到TCP数据包”事件A和“用户按键输入”事件B这两个事件中的任意一个来决定下一步做什么。#define EVENT_NET_DATA_RECV (1 0) // 位0网络数据事件 #define EVENT_KEY_PRESS (1 1) // 位1按键事件 #define EVENT_TIMER_EXPIRED (1 2) // 位2定时器事件 static rt_event_t sys_event; /* 网络中断服务程序或网络接收线程 */ void net_isr() { rt_event_send(sys_event, EVENT_NET_DATA_RECV); } /* 按键扫描线程 */ void key_scan_thread() { if (key_pressed()) { rt_event_send(sys_event, EVENT_KEY_PRESS); } } /* 主处理线程等待任意事件发生 */ void main_process_thread() { rt_uint32_t recved_events; while (1) { /* 等待三个事件中的任意一个发生并清除它 */ if (rt_event_recv(sys_event, (EVENT_NET_DATA_RECV | EVENT_KEY_PRESS | EVENT_TIMER_EXPIRED), RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR, RT_WAITING_FOREVER, recved_events) RT_EOK) { if (recved_events EVENT_NET_DATA_RECV) { handle_net_data(); } if (recved_events EVENT_KEY_PRESS) { handle_key_input(); } if (recved_events EVENT_TIMER_EXPIRED) { handle_timer(); } } } }事件集非常适合这种“多源事件触发单任务”的模型代码清晰效率高。3. RT-Thread的通信“桥梁”邮箱、消息队列与信号如果说同步机制是“交通规则”那么通信机制就是“运输工具”负责在线程间传递具体的数据或消息。3.1 邮箱固定大小的“信件投递箱”邮箱用于在线程间传递一个4字节大小的数据在32位系统上通常是一个指针。它是一种轻量级的消息传递机制。核心特性传递的是一个值通常是内存地址指针。容量固定创建时指定邮箱数量格子数。操作简单开销小。核心操作rt_mb_init()/rt_mb_create()rt_mb_send()/rt_mb_send_wait(): 发送邮件。send_wait可以设置超时如果邮箱满则等待。rt_mb_recv(): 接收邮件。使用场景传递简单的命令、状态或一个小型数据结构的指针。例如GUI线程向显示驱动线程发送一个指向待渲染图像缓冲区的指针。/* 定义一条消息结构体 */ typedef struct { rt_uint8_t cmd; void* data_ptr; } app_msg_t; static rt_mailbox_t cmd_mailbox; /* 命令发送线程 */ void commander_thread() { app_msg_t msg; msg.cmd CMD_REFRESH_SCREEN; msg.data_ptr (void*)screen_buffer; /* 将消息结构体的地址作为“邮件”发送出去 */ rt_mb_send(cmd_mailbox, (rt_ubase_t)msg); } /* 命令处理线程 */ void executor_thread() { app_msg_t *p_msg; while (1) { /* 等待并接收邮件一个指针 */ if (rt_mb_recv(cmd_mailbox, (rt_ubase_t*)p_msg, RT_WAITING_FOREVER) RT_EOK) { switch (p_msg-cmd) { case CMD_REFRESH_SCREEN: refresh_screen((screen_buffer_t*)(p_msg-data_ptr)); break; // ... 处理其他命令 } } } }注意这里传递的是msg局部变量的地址这在实际中非常危险因为commander_thread的栈帧可能很快被覆盖。务必确保通过邮箱传递的指针所指向的数据其生命周期是受控的例如来自全局内存池、静态变量或动态分配且未释放的内存。更安全的做法是传递全局数组的索引或分配自内存池的块指针。3.2 消息队列灵活强大的“数据管道”消息队列是邮箱的增强版它可以传递长度可变、内容任意的消息而不仅仅是一个4字节的值。核心特性传递的是一块内存数据。消息长度和队列容量在创建时定义。数据是复制的发送方和接收方有各自的数据副本因此对原始数据的修改不会影响已入队的消息。核心操作rt_mq_init()/rt_mq_create()rt_mq_send()/rt_mq_send_wait(): 发送消息拷贝数据到队列缓冲区。rt_mq_recv(): 接收消息从队列缓冲区拷贝数据到用户提供的缓冲区。rt_mq_urgent(): 发送紧急消息插入队列头部。使用场景需要传递复杂数据结构或较大数据块的场景。比如传感器数据采集线程将一包完整的传感器数据包含温度、湿度、压力等多个字段的结构体发送给数据处理线程。/* 定义传感器数据结构体 */ typedef struct { rt_tick_t timestamp; float temperature; float humidity; } sensor_data_t; #define MQ_MAX_MSGS 10 #define MQ_MSG_SIZE sizeof(sensor_data_t) static rt_mq_t sensor_mq; /* 数据采集线程生产者 */ void sensor_collect_thread() { sensor_data_t data; while (1) { /* 采集数据 */ data.timestamp rt_tick_get(); data.temperature read_temperature(); data.humidity read_humidity(); /* 将整个结构体数据发送到消息队列 */ rt_mq_send(sensor_mq, data, sizeof(data)); rt_thread_delay(100); } } /* 数据处理线程消费者 */ void data_process_thread() { sensor_data_t recv_data; while (1) { /* 从消息队列接收数据 */ if (rt_mq_recv(sensor_mq, recv_data, sizeof(recv_data), RT_WAITING_FOREVER) RT_EOK) { /* 处理 recv_data */ rt_kprintf([%d] Temp: %.2f, Humi: %.2f\n, recv_data.timestamp, recv_data.temperature, recv_data.humidity); } } }消息队列是生产者-消费者模型的绝佳实现解耦了数据生产者和消费者通过队列缓冲区平滑了数据流的波动。3.3 信号轻量级的“进程间通知”这里的信号Signal不同于Linux中的进程信号RT-Thread的信号是线程级别的非常轻量。它用于通知线程某个异步事件的发生类似于事件集但更简单且可以附带一个简单的整型参数。核心特性一个线程可以绑定安装多个信号处理函数。信号由其他线程或中断服务程序ISR发送。信号处理函数在接收线程的上下文中执行类似于回调函数。核心操作rt_signal_install(): 线程安装信号处理函数。rt_signal_uninstall(): 卸载。rt_thread_kill(): 向指定线程发送信号。rt_signal_mask()/rt_signal_unmask(): 屏蔽/解除屏蔽特定信号。使用场景处理一些异步的、需要特定线程响应的事件。例如看门狗中断通知某个监控线程进行系统健康检查或者一个通信线程通知主线程连接已断开。#define SIG_NET_DISCONNECT (1 0) // 自定义信号值 /* 网络监控线程的信号处理函数 */ void net_disconnect_handler(int sig) { rt_kprintf(Network disconnected! Sig: %d\n, sig); // 执行重连逻辑或状态清理 start_reconnection_procedure(); } void network_monitor_thread_entry(void *parameter) { /* 安装信号处理函数 */ rt_signal_install(SIG_NET_DISCONNECT, net_disconnect_handler); /* 进入线程主循环 */ while (1) { // ... 正常的监控逻辑 rt_thread_delay(1000); } } /* 在网络底层驱动或另一个线程中当检测到断线时 */ void some_low_level_driver() { if (link_is_lost()) { /* 向监控线程发送断线信号 */ rt_thread_kill(net_monitor_tid, SIG_NET_DISCONNECT); } }信号是一种“软中断”适用于需要打断线程当前执行流去处理紧急事件的场景。但要注意信号处理函数应尽量短小精悍避免执行耗时操作因为它会打断接收线程的正常工作。4. 实战避坑优先级反转、死锁与资源耗尽了解了工具更要了解如何安全地使用它们。在多线程编程中有些陷阱一旦踩中系统就会表现出极其诡异且难以调试的故障。4.1 优先级反转与互斥量的优先级继承这是使用二值信号量做互斥时最容易掉进去的坑也是为什么强烈推荐使用互斥量的主要原因。场景复现低优先级任务L获取了信号量S进入临界区。中优先级任务M就绪抢占了L因为M优先级高于L。此时L被挂起但它仍然持有信号量S。高优先级任务H就绪试图获取信号量S发现被L持有于是H被挂起等待。现在系统中就绪的最高优先级任务是M。M欢快地运行而H和L都在等待。H在等待L但L永远得不到运行机会被M阻塞导致H无限期等待。这就是优先级反转。解决方案使用互斥量。RT-Thread的互斥量具有优先级继承机制。在上述第3步当H尝试获取被L持有的互斥量时系统会临时将L的优先级提升到与H相同。这样L就能立即抢占M尽快执行完临界区代码并释放互斥量。一旦释放L的优先级恢复原状H此时优先级最高就能立即获取互斥量并运行。整个阻塞时间被限制在L执行临界区的时间范围内。教训保护共享资源时无脑选互斥量基本不会错。4.2 死锁当多个锁“环环相扣”死锁通常发生在需要同时持有多个资源时线程间出现了循环等待。经典死锁条件四个同时满足互斥资源一次只能被一个线程使用。持有并等待线程持有至少一个资源并等待获取其他线程持有的资源。不可剥夺资源只能由持有它的线程主动释放。循环等待存在一个线程集合 {T1, T2, ..., Tn}其中T1等待T2持有的资源T2等待T3持有的资源...Tn等待T1持有的资源。RT-Thread中的常见死锁场景/* 线程A */ void thread_a() { rt_mutex_take(mutex1, RT_WAITING_FOREVER); // 步骤1获取锁1 rt_thread_delay(10); // 模拟一些处理此时线程可能被切换 rt_mutex_take(mutex2, RT_WAITING_FOREVER); // 步骤3尝试获取锁2但被线程B持有 // ... 临界区 rt_mutex_release(mutex2); rt_mutex_release(mutex1); } /* 线程B */ void thread_b() { rt_mutex_take(mutex2, RT_WAITING_FOREVER); // 步骤2获取锁2 rt_thread_delay(10); rt_mutex_take(mutex1, RT_WAITING_FOREVER); // 步骤4尝试获取锁1但被线程A持有 // ... 临界区 rt_mutex_release(mutex1); rt_mutex_release(mutex2); }如果调度时机凑巧线程A拿了锁1线程B拿了锁2然后它们都试图去拿对方手里的锁就会陷入永恒的等待。规避死锁的实战技巧固定顺序获取所有线程都按照相同的全局顺序获取锁例如总是先获取mutex1再获取mutex2。这是最简单有效的预防方法。使用超时在rt_mutex_take、rt_sem_take等函数中使用一个合理的超时时间如RT_WAITING_FOREVER改为100个Tick而不是无限等待。超时后线程应释放已持有的所有资源回退重试或报告错误。if (rt_mutex_take(mutex2, 100) ! RT_EOK) { rt_mutex_release(mutex1); // 获取失败释放已持有的锁1 rt_kprintf(Failed to take mutex2, deadlock avoided?\n); return; // 或进行重试逻辑 }设计上减少锁的粒度重新设计数据结构和算法减少需要同时加锁的范围。比如使用读写锁RT-Thread的rwlock替代互斥量如果场景是读多写少。使用资源层级为所有资源编号线程只能申请编号更大的资源。这本质上是固定顺序的一种实现。4.3 队列满/空与超时管理防止线程永久阻塞无论是消息队列还是邮箱都有容量限制。如果生产者太快消费者太慢队列就会满反之则会空。问题rt_mq_send在队列满时默认行为是RT_WAITING_FOREVER线程会无限期挂起。同样rt_mq_recv在队列空时也会无限期挂起。解决方案使用带超时的发送/接收函数rt_mq_send_wait和rt_mq_recv本身就支持超时参数。这是最推荐的做法。/* 生产者尝试发送最多等50个Tick */ err rt_mq_send_wait(sensor_mq, data, sizeof(data), 50); if (err -RT_ETIMEOUT) { rt_kprintf(Warning: Sensor queue full, data dropped!\n); // 可以增加丢包计数或者触发流控逻辑 }非阻塞尝试将超时时间设置为0即rt_mq_send_wait(mq, ..., 0)。如果队列满函数立即返回-RT_EFULL。这适用于对实时性要求极高不能容忍任何等待的场景但需要妥善处理数据丢弃的问题。动态调整生产/消费速率这是一种更高级的流控策略。当队列快满时生产者可以主动降低数据产生频率当队列快空时消费者可以进入低功耗模式。这通常需要结合信号量或事件集来实现通知。经验之谈永远不要假设你的队列“足够大”。在系统设计初期就要根据数据流量、处理能力和内存限制估算合理的队列大小并始终为IPC操作设置一个合理的超时时间。超时处理是构建健壮嵌入式系统的必备技能。5. 机制选择与性能考量如何为你的场景挑选合适的工具面对这么多同步通信机制新手很容易犯选择困难症。下面这个决策流程和对比表格可以帮助你快速做出选择第一步明确你要做什么保护共享资源防止同时访问-首选互斥量。别用二值信号量除非你很清楚为什么不用互斥量。一个线程等待另一个线程完成某项工作-首选信号量初始为0。这是最经典的同步模式。管理有限数量的同类资源如缓冲区、连接池-计数信号量。一个线程需要等待多种不同类型事件中的任意一个或全部-事件集。需要在线程间传递一个简单的指针或整型值-邮箱。需要在线程间传递一块数据结构体、数组-消息队列。需要像中断一样紧急通知某个线程处理异步事件-信号。第二步考虑性能与开销不同的机制其内部实现复杂度和运行时开销是不同的。下面是一个简单的对比机制主要用途数据传递开销特点与注意事项信号量同步、资源计数无仅计数很低通用无所有权小心优先级反转。互斥量互斥访问无仅锁状态低有所有权和优先级继承是互斥场景的首选。事件集多事件等待无仅标志位低可等待多个事件逻辑灵活与/或。邮箱轻量消息4字节值通常是指针很低传递指针需确保数据生命周期小心野指针。消息队列通用数据传递任意长度数据块较高涉及内存拷贝数据安全拷贝但拷贝大块数据时开销大。信号异步通知一个整型参数低在目标线程上下文执行处理函数应保持简短。实战选型心得能不用通信就不用能不用同步就不用多线程带来了复杂性。首先问问自己这个数据或状态是否真的需要共享能否用线程局部存储能否用“单生产者-单消费者”无锁队列如果架构允许减少共享就是减少bug。消息队列是“万金油”但也是“性能杀手”对于高频、小数据量的通信频繁的内存拷贝会成为瓶颈。此时考虑使用邮箱传递指针配合内存池管理数据块或者使用无锁环形缓冲区。中断服务程序ISR中的使用限制在ISR中只能使用非阻塞的IPC函数通常函数名带_try后缀或者超时参数为0。例如在中断里可以向邮箱发送邮件rt_mb_send但不能等待接收rt_mb_recv可以发送事件rt_event_send但不能接收事件。这是因为ISR不能挂起等待。内存分配动态创建createIPC对象会从堆上分配内存。在资源极其紧张的系统或对确定性要求极高的场合考虑使用静态初始化init的方式将对象放在全局或静态存储区。6. 综合案例一个简易多线程数据采集系统的设计与实现让我们用一个完整的例子把上面讲的知识串起来。假设我们要设计一个环境监测节点它需要每100ms采集一次温湿度线程A。每500ms采集一次大气压力线程B。有一个数据处理线程线程C需要等到一组完整的“温湿度压力”数据后进行融合计算。计算完成后通过消息队列将结果发送给一个无线发送线程线程D。使用互斥量保护一个共享的日志文件句柄多个线程都可能写日志。系统设计线程A温湿度采集定时采集发送“温湿度就绪”事件。线程B压力采集定时采集发送“压力就绪”事件。线程C数据处理等待两个事件都发生事件集AND模式然后读取全局数据变量用互斥量保护计算将结果发送到消息队列。线程D无线发送从消息队列读取结果并发送。共享日志一个互斥量log_mutex。关键代码框架#include rtthread.h /* 定义事件标志 */ #define EVENT_TEMP_HUMI_READY (1 0) #define EVENT_PRESSURE_READY (1 1) /* 定义数据结构和IPC对象 */ typedef struct { float temp; float humi; float pressure; rt_tick_t ts; } env_data_t; static env_data_t latest_data; // 共享数据 static rt_mutex_t data_mutex; // 保护共享数据 static rt_event_t data_event; // 同步事件集 static rt_mq_t result_mq; // 结果消息队列 static rt_mutex_t log_mutex; // 日志互斥量 /* 线程C数据处理线程 */ static void data_process_thread_entry(void *param) { rt_uint32_t recv_events; env_data_t local_copy; env_data_t result; while (1) { /* 1. 等待两个采集事件都完成 */ rt_event_recv(data_event, (EVENT_TEMP_HUMI_READY | EVENT_PRESSURE_READY), RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR, RT_WAITING_FOREVER, recv_events); /* 2. 安全地拷贝共享数据 */ rt_mutex_take(data_mutex, RT_WAITING_FOREVER); local_copy latest_data; // 结构体拷贝 rt_mutex_release(data_mutex); /* 3. 进行数据融合计算示例简单平均*/ result.temp local_copy.temp; result.humi local_copy.humi; result.pressure local_copy.pressure; result.ts rt_tick_get(); // 使用处理时间作为时间戳 /* 4. 写日志受互斥量保护 */ rt_mutex_take(log_mutex, RT_WAITING_FOREVER); rt_kprintf([PROC][%d] T:%.1f H:%.1f P:%.1f\n, result.ts, result.temp, result.humi, result.pressure); rt_mutex_release(log_mutex); /* 5. 将结果发送到消息队列等待最多10个Tick */ if (rt_mq_send_wait(result_mq, result, sizeof(result), 10) ! RT_EOK) { rt_mutex_take(log_mutex, RT_WAITING_FOREVER); rt_kprintf([WARN] Result queue full, data dropped!\n); rt_mutex_release(log_mutex); } } } /* 线程A温湿度采集 */ static void temp_humi_thread_entry(void *param) { while (1) { /* 模拟采集 */ float t read_temperature_sensor(); float h read_humidity_sensor(); /* 更新共享数据 */ rt_mutex_take(data_mutex, RT_WAITING_FOREVER); latest_data.temp t; latest_data.humi h; rt_mutex_release(data_mutex); /* 发送事件通知处理线程 */ rt_event_send(data_event, EVENT_TEMP_HUMI_READY); rt_thread_delay(100); // 100ms周期 } } /* 线程B、线程D的代码类似此处省略... */ /* 初始化函数 */ int env_monitor_init(void) { /* 1. 创建互斥量 */ data_mutex rt_mutex_create(dataMutex, RT_IPC_FLAG_FIFO); log_mutex rt_mutex_create(logMutex, RT_IPC_FLAG_FIFO); /* 2. 创建事件集 */ data_event rt_event_create(dataEvent, RT_IPC_FLAG_FIFO); /* 3. 创建消息队列容量5条消息 */ result_mq rt_mq_create(resultMQ, sizeof(env_data_t), 5, RT_IPC_FLAG_FIFO); /* 4. 创建线程... */ rt_thread_create(proc, data_process_thread_entry, RT_NULL, 2048, 10, 10); rt_thread_create(th, temp_humi_thread_entry, RT_NULL, 1024, 12, 10); // ... 创建其他线程 return 0; } INIT_APP_EXPORT(env_monitor_init); // 自动初始化这个案例展示了如何混合使用互斥量保护数据、事件集同步多任务完成、消息队列传递结果以及带超时的队列操作处理队列满。在实际项目中你还需要考虑错误处理、线程优先级设置、栈大小分配以及系统负载分析。线程同步与通信是RTOS多线程编程的基石理解其原理并熟练运用RT-Thread提供的丰富机制能让你设计出既高效又稳定的嵌入式系统。记住没有最好的机制只有最适合场景的机制。从理解需求开始谨慎选择工具时刻警惕死锁和优先级反转你的多线程程序就能在资源有限的嵌入式世界里游刃有余。
返回列表