事件集
事件集是 RT-Thread 提供的另一种线程间同步机制。一个事件集可以包含多个事件,用于实现一对多、多对多的线程同步。本节将详细介绍事件集的概念、工作机制、控制块以及相关接口,并提供应用示例。
事件集概念引入
为了更好地理解事件集,我们以一个乘坐公交的场景为例进行说明:
- 场景 1: 乘客 P1 需要乘坐公交到达目的地,只有一辆特定的公交线路可以到达。P1 必须等待这辆公交车到达才能出发。
- 场景 2: 乘客 P1 需要乘坐公交到达目的地,有三条公交线路可以到达。P1 只需要等待任意一辆公交车到达即可出发。
- 场景 3: 乘客 P1 和另一名乘客 P2 约好一起乘坐公交到达目的地。P1 必须同时等待 "同伴 P2 到达公交站" 和 "公交车到达公交站" 这两个条件都满足后才能出发。
在此场景中,可以将乘客 P1 看作一个线程,将 "公交车到达公交站" 和 "同伴 P2 到达公交站" 看作发生的事件。场景 1 描述的是特定事件唤醒线程;场景 2 描述的是任意单个事件唤醒线程;场景 3 描述的是多个事件同时发生才唤醒线程。
事件集工作机制
事件集主要用于线程间的同步,与信号量不同,它具有实现一对多、多对多同步的特点。即一个线程与多个事件的关系可以是:任意一个事件发生就唤醒线程,或者所有事件都发生才唤醒线程。类似地,多个线程也可以同步多个事件。
事件集使用一个 32 位无符号整型变量表示,每一位代表一个事件。线程可以使用"逻辑与"或"逻辑或"操作将一个或多个事件关联起来,形成事件组合。
- 逻辑或 (独立型同步): 线程与任何一个事件发生同步,即只要其中一个事件发生,线程就被唤醒。
- 逻辑与 (关联型同步): 线程必须与所有关联的事件都发生同步,即只有当所有关联事件都发生后,线程才会被唤醒。
RT-Thread 事件集特点:
- 事件与线程关联,事件间独立: 每个线程可以拥有 32 个事件标志,用一个 32 位无符号整型变量表示,每一位代表一个事件。
- 事件仅用于同步,不传递数据: 事件集不具备数据传输功能。
- 事件无排队性: 多次向线程发送同一事件(如果线程还未读取)效果等同于发送一次。
每个线程拥有一个事件信息标记,包含 RT_EVENT_FLAG_AND
(逻辑与)、RT_EVENT_FLAG_OR
(逻辑或) 以及 RT_EVENT_FLAG_CLEAR
(清除标记) 三个属性。当线程等待事件同步时,通过事件标志和事件信息标记来判断是否满足同步条件。
事件集工作示意图:
事件集工作示意图
如上图所示,线程 #1 的事件标志中第 1 位和第 30 位被置位。如果事件信息标记位设置为逻辑与,则线程 #1 必须在事件 1 和事件 30 都发生后才会被唤醒。如果设置为逻辑或,则事件 1 或事件 30 中的任意一个发生都会触发唤醒线程 #1。如果同时设置了清除标记位,则线程 #1 唤醒后会将事件 1 和事件 30 清零,否则事件标志将保持置 1 状态。
事件集控制块
在 RT-Thread 中,事件集控制块是操作系统用于管理事件的数据结构,用 struct rt_event
表示。 rt_event_t
类型则表示事件集句柄,在 C 语言中是指向事件集控制块的指针。struct rt_event
的定义如下:
struct rt_event
{
struct rt_ipc_object parent; /* 继承自 ipc_object 类 */
/* 事件集合,每一 bit 表示 1 个事件,bit 位的值可以标记某事件是否发生 */
rt_uint32_t set;
};
/* rt_event_t 是指向事件结构体的指针类型 */
typedef struct rt_event* rt_event_t;
rt_event
对象继承自 rt_ipc_object
,由 IPC 容器管理。
事件集的管理方式
事件集控制块包含事件集的重要参数。事件集相关接口如下图所示,对事件集的操作包括:创建/初始化、发送事件、接收事件和删除/脱离。
事件相关接口
创建和删除事件集
创建动态事件集
使用 rt_event_create()
函数创建动态事件集:
rt_event_t rt_event_create(const char* name, rt_uint8_t flag);
调用此函数时,系统将从对象管理器中分配一个事件集对象,初始化该对象,并初始化其父类 IPC 对象。rt_event_create()
函数的参数和返回值说明如下:
参数 | 描述 |
---|---|
name |
事件集的名称。 |
flag |
事件集的标志,可以是 RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO 。 RT_IPC_FLAG_FIFO 表示非实时调度方式,建议使用 RT_IPC_FLAG_PRIO 确保线程的实时性。 |
返回值 | 描述 |
RT_NULL |
创建失败。 |
事件对象句柄 | 创建成功。 |
注意: 除非应用程序非常在意先来后到,并且清楚地知道所有涉及到该事件集的线程都会变为非实时线程,否则不建议使用 RT_IPC_FLAG_FIFO
。
删除动态事件集
使用 rt_event_delete()
函数删除由 rt_event_create()
创建的动态事件集:
rt_err_t rt_event_delete(rt_event_t event);
调用此函数时,应确保事件集不再被使用。删除操作会唤醒所有挂起在该事件集上的线程(返回值为 -RT_ERROR
),然后释放事件集对象占用的内存块。rt_event_delete()
函数的参数和返回值说明如下:
参数 | 描述 |
---|---|
event |
事件集对象句柄。 |
返回值 | 描述 |
RT_EOK |
删除成功。 |
初始化和脱离事件集
初始化静态事件集
使用 rt_event_init()
函数初始化静态事件集对象:
rt_err_t rt_event_init(rt_event_t event, const char* name, rt_uint8_t flag);
静态事件集对象的内存由编译器在编译时分配,通常放置在读写数据段或未初始化数据段中。调用该函数时,需要指定静态事件集对象的句柄,然后系统将初始化事件集对象,并将其添加到系统对象管理器中。 rt_event_init()
函数的参数和返回值说明如下:
参数 | 描述 |
---|---|
event |
事件集对象句柄。 |
name |
事件集的名称。 |
flag |
事件集的标志,可以是 RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO 。 RT_IPC_FLAG_FIFO 表示非实时调度方式,建议使用 RT_IPC_FLAG_PRIO 确保线程的实时性。 |
返回值 | 描述 |
RT_EOK |
初始化成功。 |
脱离静态事件集
使用 rt_event_detach()
函数将由 rt_event_init()
初始化的静态事件集对象从内核对象管理器中脱离:
rt_err_t rt_event_detach(rt_event_t event);
调用此函数时,系统会首先唤醒所有挂在该事件集等待队列上的线程(线程返回值为 -RT_ERROR
),然后将事件集对象从内核对象管理器中脱离。 rt_event_detach()
函数的参数和返回值说明如下:
参数 | 描述 |
---|---|
event |
事件集对象句柄。 |
返回值 | 描述 |
RT_EOK |
脱离成功。 |
发送事件
使用 rt_event_send()
函数发送一个或多个事件:
rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set);
使用此函数时,通过 set
参数指定的事件标志来设定事件集对象的事件标志值。然后,遍历等待在该事件集上的线程链表,判断是否有线程的事件激活要求与当前事件标志值匹配,如果有,则唤醒该线程。rt_event_send()
函数的参数和返回值说明如下:
参数 | 描述 |
---|---|
event |
事件集对象句柄。 |
set |
发送的一个或多个事件标志。 |
返回值 | 描述 |
RT_EOK |
发送成功。 |
接收事件
使用 rt_event_recv()
函数接收事件:
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
。rt_event_recv()
函数的参数和返回值说明如下:
参数 | 描述 |
---|---|
event |
事件集对象句柄。 |
set |
接收线程感兴趣的事件标志。 |
option |
接收选项,可以选择 "逻辑与" 或 "逻辑或",以及是否清除事件标志。 |
timeout |
指定超时时间。 |
recved |
指向接收到的事件标志的指针。 |
返回值 | 描述 |
RT_EOK |
成功接收到事件。 |
-RT_ETIMEOUT |
超时。 |
-RT_ERROR |
错误。 |
option
参数可以取以下值:
/* 选择 逻辑与 或 逻辑或 的方式接收事件 */
RT_EVENT_FLAG_OR
RT_EVENT_FLAG_AND
/* 选择清除重置事件标志位 */
RT_EVENT_FLAG_CLEAR
事件集应用示例
智能家居联动系统
开发一个智能家居系统,其中包含以下设备:
- 光照传感器: 用于检测环境光线强度。
- 窗帘电机: 用于控制窗帘的开关。
- 空调: 用于调节室内温度。
实现以下联动功能:
- 场景 1:光线充足时,自动关闭窗帘。 当光照传感器检测到环境光线强度达到一定阈值时,系统会自动关闭窗帘。
- 场景 2:光线不足时,自动开启空调。 当光照传感器检测到环境光线强度低于一定阈值时,系统会自动开启空调。
- 场景 3:用户手动打开窗帘。 用户可以通过按钮手动打开窗帘。
代码示例
#include <rtthread.h>
#include <stdlib.h> // 包含 rand 和 srand 的头文件
#include <time.h> // 包含 time 的头文件
#define THREAD_PRIORITY 9
#define THREAD_TIMESLICE 5
// 定义事件标志
#define EVENT_LIGHT_HIGH (1 << 0) // 光线强度高
#define EVENT_LIGHT_LOW (1 << 1) // 光线强度低
#define EVENT_USER_OPEN (1 << 2) // 用户手动打开窗帘
// 事件控制块
static struct rt_event event;
// 线程栈
ALIGN(RT_ALIGN_SIZE)
static char light_sensor_thread_stack[1024];
static struct rt_thread light_sensor_thread;
ALIGN(RT_ALIGN_SIZE)
static char curtain_ctrl_thread_stack[1024];
static struct rt_thread curtain_ctrl_thread;
ALIGN(RT_ALIGN_SIZE)
static char ac_ctrl_thread_stack[1024];
static struct rt_thread ac_ctrl_thread;
// 模拟光照传感器线程
static void light_sensor_thread_entry(void *parameter)
{
rt_uint32_t light_level = 0;
while (1)
{
// 模拟读取光照强度
light_level = rand() % 100; // 生成0~99的随机光照强度值
if (light_level > 70)
{
// 光线强度高,发送事件
rt_kprintf("光照传感器:光线强度高,发送 EVENT_LIGHT_HIGH 事件\n");
rt_event_send(&event, EVENT_LIGHT_HIGH);
}
else if (light_level < 30)
{
// 光线强度低,发送事件
rt_kprintf("光照传感器:光线强度低,发送 EVENT_LIGHT_LOW 事件\n");
rt_event_send(&event, EVENT_LIGHT_LOW);
}
rt_thread_mdelay(1000 + rand() % 500 ); // 模拟传感器的采样间隔
}
}
// 模拟窗帘控制线程
static void curtain_ctrl_thread_entry(void *parameter)
{
rt_uint32_t recv_event = 0;
while (1)
{
// 等待事件,光线充足或用户手动打开窗帘时关闭窗帘
if (rt_event_recv(&event, (EVENT_LIGHT_HIGH | EVENT_USER_OPEN),
RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR,
RT_WAITING_FOREVER, &recv_event) == RT_EOK)
{
if (recv_event & EVENT_LIGHT_HIGH)
{
rt_kprintf("窗帘控制器:光线充足,正在关闭窗帘\n");
// 模拟关闭窗帘
}
else if (recv_event & EVENT_USER_OPEN)
{
rt_kprintf("窗帘控制器:用户手动打开窗帘\n");
}
}
}
}
// 模拟空调控制线程
static void ac_ctrl_thread_entry(void *parameter)
{
rt_uint32_t recv_event = 0;
while (1)
{
// 等待事件,光线不足时开启空调
if (rt_event_recv(&event, EVENT_LIGHT_LOW,
RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR,
RT_WAITING_FOREVER, &recv_event) == RT_EOK)
{
if (recv_event & EVENT_LIGHT_LOW)
{
rt_kprintf("空调控制器:光线不足,正在开启空调\n");
// 模拟打开空调
}
}
}
}
// 模拟用户按钮事件 (不再导出到 MSH,直接在 main 函数中调用)
void user_open_curtain(void)
{
rt_kprintf("用户按下按钮:发送 EVENT_USER_OPEN 事件\n");
rt_event_send(&event, EVENT_USER_OPEN);
}
int main(void)
{
rt_err_t result;
// 初始化随机数种子
srand(time(NULL));
// 初始化事件集
result = rt_event_init(&event, "smart_home_event", RT_IPC_FLAG_PRIO);
if (result != RT_EOK)
{
rt_kprintf("初始化事件集失败\n");
return -1;
}
// 初始化并启动光照传感器线程
result = rt_thread_init(&light_sensor_thread,
"light_sensor",
light_sensor_thread_entry,
RT_NULL,
&light_sensor_thread_stack[0],
sizeof(light_sensor_thread_stack),
THREAD_PRIORITY, THREAD_TIMESLICE);
if(result == RT_EOK)
{
rt_thread_startup(&light_sensor_thread);
}
// 初始化并启动窗帘控制线程
result = rt_thread_init(&curtain_ctrl_thread,
"curtain_ctrl",
curtain_ctrl_thread_entry,
RT_NULL,
&curtain_ctrl_thread_stack[0],
sizeof(curtain_ctrl_thread_stack),
THREAD_PRIORITY - 1, THREAD_TIMESLICE);
if(result == RT_EOK)
{
rt_thread_startup(&curtain_ctrl_thread);
}
// 初始化并启动空调控制线程
result = rt_thread_init(&ac_ctrl_thread,
"ac_ctrl",
ac_ctrl_thread_entry,
RT_NULL,
&ac_ctrl_thread_stack[0],
sizeof(ac_ctrl_thread_stack),
THREAD_PRIORITY -1, THREAD_TIMESLICE);
if(result == RT_EOK)
{
rt_thread_startup(&ac_ctrl_thread);
}
// 模拟用户手动触发事件
rt_thread_mdelay(5000); // 等待一段时间
user_open_curtain(); // 调用用户手动打开窗帘函数
return 0;
}
实验现象
事件集的示例实验现象
事件集的使用场合
事件集适用于多种场景,可以替代信号量用于线程同步。一个线程或中断服务例程发送事件到事件集,等待的线程会被唤醒并处理相应的事件。与信号量不同,事件发送操作在事件未清除前不可累计,而信号量的释放动作是可累计的。此外,事件接收线程可以等待多个事件,选择"逻辑或"或"逻辑与"的方式触发。
多事件接收示意图:
多事件接收示意图
一个事件集包含 32 个事件,特定线程只等待并接收其关注的事件。可以是单个线程等待多个事件 (线程 1, 2),事件间使用 "与" 或 "或" 逻辑触发线程;也可以是多个线程等待同一个事件 (事件 25)。当关注的事件发生时,线程被唤醒并执行后续动作。
本文使用 markdown.com.cn 排版