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 排版

相关推荐
zyh_03052114 分钟前
GO--堆(have TODO)
数据结构·算法·golang
sjsjs1121 分钟前
【多维 DP】力扣3250. 单调数组对的数目 I
算法·leetcode
沐欣工作室_lvyiyi25 分钟前
基于单片机的无线水塔监控系统设计(论文+源码)
人工智能·stm32·单片机·嵌入式硬件·单片机毕业设计
上海文顺负载箱26 分钟前
怎样衡量电阻负载的好坏
单片机·嵌入式硬件
WangLanguager34 分钟前
基于SIFT的目标识别算法
算法
一棵星38 分钟前
EMQX构建简易的云服务
java
wkd_0071 小时前
【C++读写.xlsx文件】OpenXLSX开源库在 Ubuntu 18.04 的编译、交叉编译与使用教程
linux·ubuntu·openxlsx·c++写xlsx·开源库编译
老马啸西风1 小时前
分布式链路追踪简介-01-dapper 论文思想介绍
java
Zhu_S W1 小时前
SpringBoot项目的创建方式(五种)
java·spring boot·后端·maven·idea
Jack Yan1 小时前
【编辑器扩展】打开持久化路径/缓存路径/DataPath/StreamingAssetsPath文件夹
java·开发语言