前言
本文学习RT-Thread的事件集,事件集也是线程间同步的机制之一,一个事件集可以包含多个事件,利用事件集可以完成一对多,多对多的线程间同步。
一、事件集的基本概念
事件是一种实现线程间通信的机制,主要用于实现线程间的同步,但事件通信只能是事件类型的通信,无数据传输。与信号量不同的是,它可以实现一对多,多对多的同步。即一个线程可以等待多个事件的发生:可以是任意一个事件发生时唤醒线程进行事件处理;也可以是几个事件都发生后才唤醒线程进行事件处理。同样,事件也可以是多个线程同步多个事件。
事件集合用 32 位无符号整型变量来表示,每一位代表一个事件,线程通过“逻辑与”或“逻辑或”与一个或多个事件建立关联,形成一个事件集。事件的“逻辑或”也称作是独立型同步,指的是线程感兴趣的所有事件任一件发生即可被唤醒;事件“逻辑与”也称为是关联型同步,指的是线程感兴趣的若干事件都发生时才被唤醒。
多线程环境下,线程之间往往需要同步操作,一个事件发生即是一个同步。事件可以提供一对多、多对多的同步操作。一对多同步模型:一个线程等待多个事件的触发;多对多同步模型:多个线程等待多个事件的触发。线程可以通过创建事件来实现事件的触发和等待操作。RT-Thread 的事件仅用于同步,不提供数据传输功能。
RT-Thread 提供的事件具有如下特点:
- 事件只与线程相关联,事件相互独立,一个 32 位的事件集合(set 变量),用于标识该线程发生的事件类型,其中每一位表示一种事件类型(0 表示该事件类型未发生、1 表示该事件类型已经发生),一共 32 种事件类型。
- 事件仅用于同步,不提供数据传输功能。
- 事件无排队性,即多次向线程发送同一事件(如果线程还未来得及读走),等效于只发送一次。
- 允许多个线程对同一事件进行读写操作。
- 支持事件等待超时机制。
二、事件的应用场景
RT-Thread 的事件用于事件类型的通讯,无数据传输,也就是说,我们可以用事件来做标志位,判断某些事件是否发生了,然后根据结果做处理,那很多人又会问了,为什么我不直接用变量做标志呢,岂不是更好更有效率?非也非也,若是在裸机编程中,用全局变量是最为有效的方法,这点我不否认,但是在操作系统中,使用全局变量就要考虑以下问题了:
- 如何对全局变量进行保护呢,防止多线程同时对它进行访问?
- 如何让内核对事件进行有效管理呢?使用全局变量的话,就需要在线程中轮询查看事件是否发送,这简直就是在浪费 CPU 资源啊,还有等待超时机制,使用全局变量的话需要用户自己去实现。
所以,在操作系统中,还是使用操作系统给我们提供的通信机制就好了,简单方便还实用。
在某些场合,可能需要多个时间发生了才能进行下一步操作,比如一些危险机器的启动,需要检查各项指标,当指标不达标的时候,无法启动,但是检查各个指标的时候,不能一下子检测完毕,所以,需要事件来做统一的等待,当所有的事件都完成了,那么机器才允许启动,这只是事件的其中一个应用。
事件可使用于多种场合,它能够在一定程度上替代信号量,用于线程间同步。一个线程或中断服务例程发送一个事件给事件对象,而后等待的线程被唤醒并对相应的事件进行处理。但是它与信号量不同的是,事件的发送操作是不可累计的,而信号量的释放动作是可累计的。事件另外一个特性是,接收线程可等待多种事件,即多个事件对应一个线程或多个线程。同时按照线程等待的参数,可选择是“逻辑或”触发还是“逻辑与”触发。这个特性也是信号量等所不具备的,信号量只能识别单一同步动作,而不能同时等待多个事件的同步。
各个事件可分别发送或一起发送给事件对象,而线程可以等待多个事件,线程仅对感兴趣的事件进行关注。当有它们感兴趣的事件发生时并且符合感兴趣的条件,线程将被唤醒并进行后续的处理动作
三、事件集的工作机制
1、一个线程与多个事件的关系可设置为:其中任意一个事件唤醒线程,或几个事件都到达后才唤醒线程进行后续的处理;同样,事件也可以是多个线程同步多个事件。
2、RT-Thread 定义的事件集有以下特点:(1)事件只与线程相关,事件间相互独立。(2)事件仅用于同步,不提供数据传输功能;(3)事件无排队性,即多次向线程发送同一事件 (如果线程还未来得及读走),其效果等同于只发送一次。
3、每个线程都拥有一个事件信息标记,它有三个属性,分别是 RT_EVENT_FLAG_AND(逻辑与), RT_EVENT_FLAG_OR(逻辑或)以及 RT_EVENT_FLAG_CLEAR(清除标记)。当线程等待事件同步时,可以通过 32 个事件标志和这个事件信息标记来判断当前接收的事件是否满足同步条件。
如上图所示,线程 #1 的事件标志中第 1 位和第 30 位被置位,如果事件信息标记位设为逻辑与,则表示线程 #1 只有在事件 1 和事件 30 都发生以后才会被触发唤醒,如果事件信息标记位设为逻辑或,则事件 1 或事件 30 中的任意一个发生都会触发唤醒线程 #1。如果信息标记同时设置了清除标记位,则当线程 #1 唤醒后将主动把事件 1 和事件 30 清为零,否则事件标志将依然存在(即置 1)。
四、事件集的相关函数
1、创建动态事件集函数
当创建一个事件集时,内核首先创建一个事件集控制块,然后对该事件集控制块进行基本的初始化。
rt_event_t rt_event_create(const char* name, rt_uint8_t flag);
(1)入口参数:
name:事件集的名称。
flag:事件集的标志,它可以取如下数值:RT_IPC_FLAG_FIFO 或RT_IPC_FLAG_PRIO。
(2)返回值:
RT_NULL:创建失败。
事件对象的句柄:创建成功。
2、删除动态事件函数
系统不再使用 rt_event_create() 创建的事件集对象时,通过删除事件集对象控制块来释放系统资源。在删除一个事件集对象时,应该确保该事件集不再被使用。在删除前会唤醒所有挂起在该事件集上的线程(线程的返回值是RT_ERROR),然后释放事件集对象占用的内存块。
rt_err_t rt_event_delete(rt_event_t event);
(1)入口参数:
event:事件集对象的句柄。
(2)返回值:
RT_EOK:成功。
3、创建静态事件集函数
这里所说的创建静态事件集和《RT-Thread编程指南》所说的初始化事件集是一样的,静态事件集对象的内存是在系统编译时由编译器分配的,一般放于读写数据段或未初始化数据段中。
rt_err_t rt_event_init(rt_event_t event, const char* name, rt_uint8_t flag);
(1)入口参数:
event:事件集对象的句柄。
name:事件集的名称。
flag:事件集的标志,它可以取如下数值:RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO。
(2)返回值:
RT_EOK:成功。
4、删除静态事件集函数
当系统不再使用 rt_event_init() 初始化的事件集对象时,通过脱离事件集对象控制块来释放系统资源,系统首先唤醒所有挂在该事件集等待队列上的线程(线程的返回值是RT_ERROR),然后将该事件集从内核对象管理器中脱离。
rt_err_t rt_event_detach(rt_event_t event);
(1)入口参数:
event:事件集对象的句柄。
(2)返回值:
RT_EOK:成功。
5、发送事件函数
发送事件函数可以发送事件集中的一个或多个事件,使用该函数接口时,通过参数 set 指定的事件标志来设定 event 事件集对象的事件标志值,然后遍历等待在 event 事件集对象上的等待线程链表,判断是否有线程的事件激活要求与当前 event 对象事件标志值匹配,如果有,则唤醒该线程。
rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set);
(1)入口参数:
event:事件集对象的句柄。
set:发送的一个或多个事件的标志值。
(2)返回值:
RT_EOK:成功。
6、接收事件函数
这里是引用内核使用 32 位的无符号整数来标识事件集,它的每一位代表一个事件,因此一个事件集对象可同时等待接收 32 个事件,内核可以通过指定选择参数 “逻辑与” 或 “逻辑或” 来选择如何激活线程,使用 “逻辑与” 参数表示只有当所有等待的事件都发生时才激活线程,而使用 “逻辑或” 参数则表示只要有一个等待的事件发生就激活线程。
系统首先根据 set 参数和接收选项 option 来判断它要接收的事件是否发生,如果已经发生,则根据参数 option 上是否设置有 RT_EVENT_FLAG_CLEAR 来决定是否重置事件的相应标志位,然后返回(其中 recved 参数返回接收到的事件);如果没有发生,则把等待的 set 和 option 参数填入线程本身的结构中,然后把线程挂起在此事件上,直到其等待的事件满足条件或等待时间超过指定的超时时间。如果超时时间设置为零,则表示当线程要接受的事件没有满足其要求时就不等待,而直接返回RT_ETIMEOUT。
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);
(1)入口参数:
event:事件集对象的句柄。
set:接收线程感兴趣的事件。
option:接收选项。
timeout:指定超时时间。
recved:指向接收到的事件。
(2)返回值:
RT_EOK:成功。
RT_ETIMEOUT:超时。
RT_ERROR:错误。
五、事件集实验
事件标志组实验是在 RT-Thread 中创建了两个线程,一个是发送事件线程,一个是接收事件线程,两个线程独立运行,发送事件线程通过检测按键的按下情况发送不同的事件,接收事件线程则接收这两个事件,并且判断两个事件是否都发生,如果是则输出相应信息,LED 进行翻转。接收线程的等待时间是 RT_WAITING_FOREVER,一直在等待事件的发生,接收事件之后进行清除事件标记
#include "event.h"
#include "board.h"
/*
******************************************************************
* 变量
******************************************************************
*/
/* 定义线程控制块 */
static rt_thread_t receive_thread = RT_NULL;
static rt_thread_t send_thread = RT_NULL;
/* 定义事件控制块(句柄) */
static rt_event_t test_event = RT_NULL;
/************************* 全局变量声明 ****************************/
/*
* 当我们在写应用程序的时候,可能需要用到一些全局变量。
*/
#define KEY1_EVENT (0x01 << 0)//设置事件掩码的位0
#define KEY2_EVENT (0x01 << 1)//设置事件掩码的位1
/*
*************************************************************************
* 函数声明
*************************************************************************
*/
static void receive_thread_entry(void* parameter);
static void send_thread_entry(void* parameter);
/*
*************************************************************************
* main 函数
*************************************************************************
*/
/**
* @brief 主函数
* @param 无
* @retval 无
*/
int Event_Init(void)
{
/*
* 开发板硬件初始化,RTT系统初始化已经在main函数之前完成,
* 即在component.c文件中的rtthread_startup()函数中完成了。
* 所以在main函数中,只需要创建线程和启动线程即可。
*/
rt_kprintf("这是一个RTT事件标志组实验!\n");
/* 创建一个事件 */
test_event = rt_event_create("test_event", /* 事件标志组名字 */
RT_IPC_FLAG_PRIO); /* 事件模式 另一个为FIFO(0x00)*/
if (test_event != RT_NULL)
rt_kprintf("事件创建成功!\n\n");
receive_thread = /* 线程控制块指针 */
rt_thread_create( "receive", /* 线程名字 */
receive_thread_entry, /* 线程入口函数 */
RT_NULL, /* 线程入口函数参数 */
512, /* 线程栈大小 */
3, /* 线程的优先级 */
20); /* 线程时间片 */
/* 启动线程,开启调度 */
if (receive_thread != RT_NULL)
rt_thread_startup(receive_thread);
else
return -1;
send_thread = /* 线程控制块指针 */
rt_thread_create( "send", /* 线程名字 */
send_thread_entry, /* 线程入口函数 */
RT_NULL, /* 线程入口函数参数 */
512, /* 线程栈大小 */
2, /* 线程的优先级 */
20); /* 线程时间片 */
/* 启动线程,开启调度 */
if (send_thread != RT_NULL)
rt_thread_startup(send_thread);
else
return -1;
}
/*
*************************************************************************
* 线程定义
*************************************************************************
*/
static void receive_thread_entry(void* parameter)
{
rt_uint32_t recved;
/* 任务都是一个无限循环,不能返回 */
while(1)
{
/* 等待接收事件标志 */
rt_event_recv(test_event, /* 事件对象句柄 */
KEY1_EVENT|KEY2_EVENT, /* 接收线程感兴趣的事件 */
RT_EVENT_FLAG_AND|RT_EVENT_FLAG_CLEAR, /* 接收选项,接收完后清除事件标志 */
RT_WAITING_FOREVER, /* 指定超时事件,一直等 */
&recved); /* 指向接收到的事件 */
if(recved == (KEY1_EVENT|KEY2_EVENT)) /* 如果接收完成并且正确 */
{
rt_kprintf ( "Key1与Key2都按下\n");
LED1_TOGGLE; //LED1 反转
}
else
rt_kprintf ( "事件错误!\n");
}
}
static void send_thread_entry(void* parameter)
{
/* 任务都是一个无限循环,不能返回 */
while (1)
{
if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ) //如果KEY2被单击
{
rt_kprintf ( "KEY1被单击\n" );
/* 发送一个事件1 */
rt_event_send(test_event,KEY1_EVENT);
}
if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON ) //如果KEY2被单击
{
rt_kprintf ( "KEY2被单击\n" );
/* 发送一个事件2 */
rt_event_send(test_event,KEY2_EVENT);
}
rt_thread_delay(20); //每20ms扫描一次
}
}
实验现象:rtthread事件集实验
另一个实验:
//------------------------------------------------------------
// 实验二
//------------------------------------------------------------
#if 0
/* 线程句柄 */
static rt_thread_t thread1 = NULL;
static rt_thread_t thread2 = NULL;
/* 事件集句柄 */
static rt_event_t event1 = NULL;
#define EVENT_FLAG3 (1 << 3)
#define EVENT_FLAG5 (1 << 5)
/**************************************************************
函数名称: thread1_recv_event
函数功能: 线程1入口函数,用于接收事件
输入参数: parameter:入口参数
返 回 值: 无
备 注: 无
**************************************************************/
void thread1_recv_event(void *parameter)
{
rt_uint32_t recved;
while(1)
{
/* 事件3或事件5任意一个可以触发线程,接收完后清除事件标志 */
if(RT_EOK == rt_event_recv(event1,
(EVENT_FLAG3 | EVENT_FLAG5),
RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR,
RT_WAITING_FOREVER,
&recved))
{
rt_kprintf("thread1: OR recv event 0x%x\n", recved);
BEEP(1);
rt_thread_mdelay(300);
BEEP(0);
rt_thread_mdelay(300);
}
rt_thread_mdelay(1000);
/* 事件3和事件5都触发才可以触发线程,接收完后清除事件标志 */
if(RT_EOK == rt_event_recv(event1,
(EVENT_FLAG3 | EVENT_FLAG5),
RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR,
RT_WAITING_FOREVER,
&recved))
{
rt_kprintf("thread1: AND recv event 0x%x\n", recved);
LED_R(0);
rt_thread_mdelay(1000);
LED_R(1);
rt_thread_mdelay(1000);
}
}
}
/**************************************************************
函数名称: thread2_send_event
函数功能: 线程2入口函数,用于发送事件
输入参数: parameter:入口参数
返 回 值: 无
备 注: 无
**************************************************************/
void thread2_send_event(void *parameter)
{
u8 key;
while(1)
{
key = key_scan(0);
if( key== KEY0_PRES)
{
rt_event_send(event1, EVENT_FLAG3);
rt_kprintf("RT-Thread send EVENT_FLAG3\r\n");
}
else if(key == KEY1_PRES)
{
rt_event_send(event1, EVENT_FLAG5);
rt_kprintf("RT-Thread send EVENT_FLAG5\r\n");
}
rt_thread_mdelay(1);
}
}
void rtthread_event_test(void)
{
event1 = rt_event_create("event1", RT_IPC_FLAG_PRIO);/* FIFO模式 */
/* 创建事件集 */
if(event1 != RT_NULL)
{
rt_kprintf("RT-Thread create event successful\r\n");
}
else
{
rt_kprintf("RT-Thread create event failed\r\n");
return;
}
thread1 = rt_thread_create("thread1",
thread1_recv_event,
NULL,
512,
3,
20);
if(thread1 != RT_NULL)
{
rt_thread_startup(thread1);;
}
else
{
rt_kprintf("create thread1 failed\r\n");
return;
}
thread2 = rt_thread_create("thread2",
thread2_send_event,
NULL,
512,
2,
20);
if(thread2 != RT_NULL)
{
rt_thread_startup(thread2);;
}
else
{
rt_kprintf("create thread2 failed\r\n");
return;
}
}
#endif
(1)当按下KEY0或KEY1时,发送事件标志FLAG3或FLAG5,这个时候蜂鸣器响:
(2)当按下KEY0和KEY1时,发送事件标志FLAG3和FLAG5,这个时候RGB红灯亮一下:
参考文献:
1、[野火®]《RT-Thread 内核实现与应用开发实战—基于STM32》
2、《RT-THREAD 编程指南》