RTOS之事件集

事件集

事件集是 RT-Thread 提供的另一种线程间同步机制。一个事件集可以包含多个事件,用于实现一对多、多对多的线程同步。本节将详细介绍事件集的概念、工作机制、控制块以及相关接口,并提供应用示例。

事件集概念引入

为了更好地理解事件集,我们以一个乘坐公交的场景为例进行说明:

  • 场景 1: 乘客 P1 需要乘坐公交到达目的地,只有一辆特定的公交线路可以到达。P1 必须等待这辆公交车到达才能出发。
  • 场景 2: 乘客 P1 需要乘坐公交到达目的地,有三条公交线路可以到达。P1 只需要等待任意一辆公交车到达即可出发。
  • 场景 3: 乘客 P1 和另一名乘客 P2 约好一起乘坐公交到达目的地。P1 必须同时等待 "同伴 P2 到达公交站" 和 "公交车到达公交站" 这两个条件都满足后才能出发。

在此场景中,可以将乘客 P1 看作一个线程,将 "公交车到达公交站" 和 "同伴 P2 到达公交站" 看作发生的事件。场景 1 描述的是特定事件唤醒线程;场景 2 描述的是任意单个事件唤醒线程;场景 3 描述的是多个事件同时发生才唤醒线程。

事件集工作机制

事件集主要用于线程间的同步,与信号量不同,它具有实现一对多、多对多同步的特点。即一个线程与多个事件的关系可以是:任意一个事件发生就唤醒线程,或者所有事件都发生才唤醒线程。类似地,多个线程也可以同步多个事件。

事件集使用一个 32 位无符号整型变量表示,每一位代表一个事件。线程可以使用"逻辑与"或"逻辑或"操作将一个或多个事件关联起来,形成事件组合。

  • 逻辑或 (独立型同步): 线程与任何一个事件发生同步,即只要其中一个事件发生,线程就被唤醒。
  • 逻辑与 (关联型同步): 线程必须与所有关联的事件都发生同步,即只有当所有关联事件都发生后,线程才会被唤醒。

RT-Thread 事件集特点:

  1. 事件与线程关联,事件间独立: 每个线程可以拥有 32 个事件标志,用一个 32 位无符号整型变量表示,每一位代表一个事件。
  2. 事件仅用于同步,不传递数据: 事件集不具备数据传输功能。
  3. 事件无排队性: 多次向线程发送同一事件(如果线程还未读取)效果等同于发送一次。

每个线程拥有一个事件信息标记,包含 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_FIFORT_IPC_FLAG_PRIORT_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_FIFORT_IPC_FLAG_PRIORT_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 参数返回接收到的事件)。如果没有发生,则将等待的 setoption 参数填入线程结构中,然后将线程挂起在此事件上,直到满足条件或超时。 如果超时时间设置为零,则当线程要接收的事件没有满足其要求时,不会等待,而是直接返回 -RT_ETIMEOUTrt_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. 场景 1:光线充足时,自动关闭窗帘。 当光照传感器检测到环境光线强度达到一定阈值时,系统会自动关闭窗帘。
  2. 场景 2:光线不足时,自动开启空调。 当光照传感器检测到环境光线强度低于一定阈值时,系统会自动开启空调。
  3. 场景 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 排版

相关推荐
hrrrrb9 小时前
【Spring Security】Spring Security 概念
java·数据库·spring
小信丶9 小时前
Spring 中解决 “Could not autowire. There is more than one bean of type“ 错误
java·spring
点灯小铭10 小时前
基于单片机的智能水箱温度液位控制系统设计
单片机·嵌入式硬件·毕业设计·课程设计
周杰伦_Jay10 小时前
【Java虚拟机(JVM)全面解析】从原理到面试实战、JVM故障处理、类加载、内存区域、垃圾回收
java·jvm
星梦清河10 小时前
宋红康 JVM 笔记 Day18|class文件结构
jvm
晓风残月淡10 小时前
JVM字节码与类的加载(二):类加载器
jvm·python·php
用手手打人10 小时前
JVM(十)-- 类的加载器
jvm
未来之窗软件服务14 小时前
自己写算法(九)网页数字动画函数——东方仙盟化神期
前端·javascript·算法·仙盟创梦ide·东方仙盟·东方仙盟算法
程序员小凯14 小时前
Spring Boot测试框架详解
java·spring boot·后端
ANYOLY14 小时前
JVM 面试宝典
jvm