【RT-Thread学习笔记】初步学习线程间的同步(三):事件

张开发
2026/4/12 17:12:15 15 分钟阅读

分享文章

【RT-Thread学习笔记】初步学习线程间的同步(三):事件
一、前言在前面的学习中我们已经掌握了信号量与互斥量的核心用法信号量主要用于线程间的一对一同步也能实现简易的资源互斥互斥量则专门解决共享资源的独占访问问题弥补了信号量做互斥时无优先级继承、易锁死的缺陷。但在实际嵌入式开发中我们常会遇到更复杂的场景——一个线程需要等待多个事件中的任意一个触发或者必须等所有事件全部发生后再执行比如线程需要同时响应按键触发、串口数据接收两个事件此时信号量和互斥量就排不上用场了。今天我们就来学习RTT线程间同步的第三种核心工具——事件标志组它专为多事件同步场景设计能轻松实现“一个线程等待多个事件”的需求让线程间的协作更灵活、更高效。二、事件集事件集的含义事件集也是线程间同步的机制之一应该事件集可以包含多个事件利用事件集可以完成一对多多对多的线程同步。下面以RTT官方内核中的例子说明。在公交车站等公交时可能有以下几种情况1、P1坐公交车去某地只有一种公交可以到达目的地等到此公交即可触发。2、P1坐公交车去某地有三种公交都可以到达目的地等到其中任意一辆即刻出发。3、P1约另一人P2一起去某地则P1必须要等到“同伴P2到达公交站”与“公交到达公交站”两个条件都满足后才能出发。这里可以将P1去某地视为线程将“公交到达公交站”、“同伴P2到达公交站”视为事件的发生情况1是特定事件唤醒线程情况2是任意单个事件唤醒线程情况3是多个事件同时发生才唤醒线程。事件集工作机制事件集主要用于线程间的同步与信号量不同它的特点是可以实现一对多多对多的同步。即一个线程与多个事件的关系可设置为其中任意一个事件唤醒线程或几个事件都到达后才唤醒线程进行后续的处理同样事件也可以是多个线程多个事件。这种多个事件的集合可以用一个32位无符号整型变量来表示变量的每一位代表一个事件线程通过“逻辑与”或“逻辑或”将一个或多个事件关联起来形成事件组合。时间的“逻辑或”也成为是独立型同步指的是线程与任何事件之一发生同步事件”逻辑或“也称为是关联型同步指的是线程与若干事件都发生同步。RTT定义的事件集有以下特点1、事件只与线程相关事件间相互独立每个线程可拥有32个事件标志采用一个32bit无符号整型数进行记录每个bit代表一个事件2、事件仅用于同步不提供数据传输功能3、事件无排队性即多次向线程发送同一事件如果线程还未来得及读走其效果等同与只发送一次。如上图所示线程#1的事件标志中第1位和第28位被置位如果事件信息标志位设为逻辑与则表示线程 #1 只有在事件 1 和事件 28 都发生以后才会被触发唤醒如果事件信息标记位设为逻辑或则事件 1 或事件 28 中的任意一个发生都会触发唤醒线程 #1。如果信息标记同时设置了清除标记位则当线程 #1 唤醒后将主动把事件 1 和事件28清为零否则事件标志将依然存在即置 1。三、RT-Thread 事件集 API 详解1、创建事件集当创建一个事件集时内核首先创建一个事件集控制块然后对该事件集控制块进行基本的初始化创建事件集使用下面的函数接口rt_event_t rt_event_create(const char* name, rt_uint8_t flag);参数描述name事件集的名称flag事件集的标志它可以取如下数值 RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO返回——RT_NULL创建失败事件对象的句柄创建成功2、删除事件集系统不再使用rt_event_create() 创建的事件集对象时通过删除事件集对象控制块来释放系统资源。删除事件集可以用如下函数接口rt_err_t rt_event_delete(rt_event_t event);参数描述event事件集对象的句柄返回——RT_EOK成功在调用 rt_event_delete 函数删除一个事件集对象时应该确保该事件集不再被使用。在删除前会唤醒所有挂起在该事件集上的线程线程的返回值是 - RT_ERROR然后释放事件集对象占用的内存块。3、初始化事件集静态事件集对象的内存时在系统编译时由编译器分配的一般放于读写数据段或未初始化数据段中。在使用静态事件集对象前需要先对它进行初始化操作。初始化事件集使用下面的函数接口rt_err_t rt_event_init(rt_event_t event, const char* name, rt_uint8_t flag);使用该接口时需指定静态事件集对象的句柄即指向事件集控制块的指针然后系统会初始化事件集对象并加入到系统容器中进行管理。参数描述event事件集对象的句柄name事件集的名称flag事件集的标志它可以取如下数值 RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO返回——RT_EOK成功4、脱离事件集系统不再使用 rt_event_init() 初始化的事件集对象时通过脱离事件集对象控制块来释放系统资源。脱离事件集是将事件集对象从内核对象管理器中脱离。脱离事件集使用下面的函数接口rt_err_t rt_event_detach(rt_event_t event);用户调用这个函数时系统首先唤醒所有挂在该事件集等待队列上的线程然后将该事件集从内核对象管理器中脱离。参数描述event事件集对象的句柄返回——RT_EOK成功5、发送事件发送事件函数可以发送事件集中的一个或多个事件如下rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set);使用该函数接口时通过参数 set 指定的事件标志来设定 event 事件集对象的事件标志值然后遍历等待在 event 事件集对象上的等待线程链表判断是否有线程的事件激活要求与当前 event 对象事件标志值匹配如果有则唤醒该线程。参数描述event事件集对象的句柄set发送的一个或多个事件的标志值返回——RT_EOK成功6、接收事件内核使用 32 位的无符号整数来标识事件集它的每一位代表一个事件因此一个事件集对象可同时等待接收 32 个事件内核可以通过指定选择参数 “逻辑与” 或“逻辑或”来选择如何激活线程使用 “逻辑与” 参数表示只有当所有等待的事件都发生时才激活线程而使用 “逻辑或” 参数则表示只要有一个等待的事件发生就激活线程。接收事件使用下面的函数接口rt_err_t rt_event_recv(rt_event_t event, rt_uint32_t set, rt_uint8_t option, rt_int32_t timeout, rt_uint32_t* recved);当用户调用这个接口时系统首先根据 set 参数和接收选项 option 来判断它要接收的事件是否发生如果已经发生则根据参数 option 上是否设置有 RT_EVENT_FLAG_CLEAR 来决定是否重置事件的相应标志位然后返回其中 recved 参数返回接收到的事件如果没有发生则把等待的 set 和 option 参数填入线程本身的结构中然后把线程挂起在此事件上直到其等待的事件满足条件或等待时间超过指定的超时时间。如果超时时间设置为零则表示当线程要接受的事件没有满足其要求时就不等待而直接返回 - RT_ETIMEOUT。参数描述event事件集对象的句柄set接收线程感兴趣的事件option接收选项/* 选择 逻辑与 或 逻辑或 的方式接收事件 */RT_EVENT_FLAG_OR RT_EVENT_FLAG_AND /*选择清除重置事件标志位 */RT_EVENT_FLAG_CLEARtimeout指定超时时间recved指向接收到的事件返回——RT_EOK成功-RT_ETIMEOUT超时-RT_ERROR错误四、实例演示#include rtthread.h #include rtdevice.h #include board.h #define EVENT_FLAG3 (13) #define EVENT_FLAG5 (15) /*事件控制块*/ static struct rt_event event; ALIGN(RT_ALIGN_SIZE) static char thread1_stack[1024]; static struct rt_thread thread1; /*线程1入口函数*/ static void thread1_recv_event(void*parameter) { rt_uint32_t e; /*第一次接收事件事件3或事件5任意一个即可触发线程1接收完后清除事件标志*/ if (rt_event_recv(event, (EVENT_FLAG3|EVENT_FLAG5), RT_EVENT_FLAG_OR|RT_EVENT_FLAG_CLEAR, RT_WAITING_FOREVER, e) RT_EOK) { rt_kprintf(thread1:OR recv event 0x%x\n,e); } rt_kprintf(thread1 : delay 1s to prepare the second event\n); rt_thread_mdelay(1000); /*第二次接收事件事件3和事件5均发生时才可以触发线程1接受完后清除事件标志*/ if (rt_event_recv(event, (EVENT_FLAG3|EVENT_FLAG5), RT_EVENT_FLAG_AND|RT_EVENT_FLAG_CLEAR, RT_WAITING_FOREVER, e) RT_EOK) { rt_kprintf(thread1:AND recv event 0x%x\n,e); } /*执行完该事件集后进行事件集的脱离事件集重复初始化会导致再次运行时出现重复初始化的问题*/ rt_event_detach(event); rt_kprintf(thread1 leave.\n); } ALIGN(RT_ALIGN_SIZE) static char thread2_stack[1024]; static struct rt_thread thread2; /*线程2入口*/ static void thread2_send_event(void*parameter) { rt_kprintf(thread2:send event3\n); rt_event_send(event, EVENT_FLAG3); rt_thread_mdelay(200); rt_kprintf(thread2: send event5\n); rt_event_send(event, EVENT_FLAG5); rt_thread_mdelay(200); rt_kprintf(thread2: send event3\n); rt_event_send(event, EVENT_FLAG3); rt_kprintf(thread2 leave.\n); } int event_sample(void) { rt_err_t result; /* 初始化事件对象 */ result rt_event_init(event, event, RT_IPC_FLAG_PRIO); if (result ! RT_EOK) { rt_kprintf(init event failed.\n); return -1; } rt_thread_init(thread1, thread1, thread1_recv_event, RT_NULL, thread1_stack[0], sizeof(thread1_stack), 8, 5); rt_thread_startup(thread1); rt_thread_init(thread2, thread2, thread2_send_event, RT_NULL, thread2_stack[0], sizeof(thread2_stack), 9, 5); rt_thread_startup(thread2); return 0; } int main(void) { event_sample(); while (1) { rt_thread_mdelay(1000); } return 0; }这是RTT官方展示的示例代码主要演示了事件标志组的两种核心等待方式。线程1负责接收事件线程2负责发送事件通过事件实现线程间的精准同步。1、定义两个事件标志#define EVENT_FLAG3 (13) #define EVENT_FLAG5 (15)13 二进制1000 事件315 二进制1000 事件5事件标志组的每一位代表一个独立事件2、定义事件控制块static struct rt_event event;定义一个静态事件对象用来管理事件的发送、接收、等待3、定义线程1接收事件线程ALIGN(RT_ALIGN_SIZE) static char thread1_stack[1024]; static struct rt_thread thread1;给线程1分配栈空间1024字节定义线程控制块4、线程1入口函数核心接收事件static void thread1_recv_event(void*parameter)线程1用来等待事件分两步等事件3OR事件5等事件3AND事件54.1第一次接收OR模式任意来一个事件就行rt_event_recv( event, // 事件对象 EVENT_FLAG3|EVENT_FLAG5, // 等待事件3 或 5 RT_EVENT_FLAG_OR | // OR 模式任意一个来就唤醒 RT_EVENT_FLAG_CLEAR, // 接收完自动清除事件标志 RT_WAITING_FOREVER, // 永久等待 e // 保存收到的事件值 );只要事件3或者事件5任意来一个就行线程就唤醒4.2第二次接收AND模式必须两个事件都来rt_event_recv( event, EVENT_FLAG3|EVENT_FLAG5, RT_EVENT_FLAG_AND | // AND必须两个事件都来 RT_EVENT_FLAG_CLEAR, RT_WAITING_FOREVER, e );必须事件3和事件5都发生线程才唤醒。4.3事件脱离rt_event_detach(event);静态初始化的事件用完后需要脱离避免重复初始化报错5、定义线程2发送事件线程ALIGN(RT_ALIGN_SIZE) static char thread2_stack[1024]; static struct rt_thread thread2;6、线程2入口函数发送事件static void thread2_send_event(void*parameter)线程2用来发送事件顺序发送事件 3发送事件 5再发送事件 37、 初始化事件 创建线程int event_sample(void) rt_event_init(event, event, RT_IPC_FLAG_PRIO); rt_thread_init(thread1, thread1, ... , 优先级8); rt_thread_init(thread2, ... , 优先级9);线程1比线程2优先级高会先运行8、最终运行流程线程1先运行,等待第一次事件OR—线程2发送事件3线程1唤醒—线程1延时1秒—线程1开始第二次等待AND—线程2发送事件3、事件5—两个事件都到了线程1唤醒—程序结束这是串口打印成功的结果可以看到是按照运行流程来的。五、总结通过本次事件集的学习与实战我们掌握了 RT-Thread 事件标志组的核心用法。事件集是一种多线程同步工具用一个 32 位标志值中的每一位表示一个独立事件支持一个线程等待多个事件。它提供 OR任意事件和AND所有事件两种等待模式可灵活实现多触发条件同步。相比信号量只能一对一通知事件集能够同时管理多个事件极大提升了多任务协作的灵活性。在实际开发中事件集常用于按键、传感器、串口等多源事件的统一等待与处理是嵌入式多任务系统中非常实用的同步机制。

更多文章