书史足自悦,安用勤与劬。这篇文章主要讲述RT-Thread快速入门-事件集相关的知识,希望能为你提供帮助。
RT-Thread 中的事件集,也就是其他 RTOS 中的事件标志组。事件集也是线程(任务)间同步的一种机制。
【RT-Thread快速入门-事件集】前面介绍的两种线程间同步的方式(信号量和互斥量)都是一对一;而事件集可以实现一对多、多对多的线程同步。
事件集的工作机制
1. 理解事件集
多个事件的集合用一个 32 位无符号整型变量来表示,变量的每一位代表一个事件,线程通过 “逻辑与” 或 “逻辑或” 将一个或多个事件关联起来,形成事件组合。
RT-Thread 中的事件集有以下特点:
- 事件只与线程相关,事件间相互独立。
- 事件仅用于同步,不提供数据传输功能
- 事件无排队性,即多次发送同一个事件(且线程还未读走),其效果等同于发送了一次。
举例理解事件集,如下图所示,线程 #1 的事件标志中第 1 位和第 30 位被置位,等待事件发生。
文章图片
- 逻辑与,则表示线程 #1 只有在事件 1 和事件 30 都发生以后才会被触发唤醒。
- 逻辑或,则事件1 或事件 30 中的任意一个发生都会触发唤醒线程 #1。
- 同时设置了清除标记位,则当线程 #1 唤醒后将主动把事件 1 和事件 30 清为零,否则事件标志将依然存在(即置 1)。
在 RT-Thread 中,操作系统管理事件的数据结构称为事件集控制块,由结构体
struct rt_event
表示。另外,rt_event_t
表示的是事件集的句柄,即指向事件集控制块的指针。事件集控制块结构体定义如下:
struct rt_event/* 继承自 ipc_object 类 */
struct rt_ipc_object parent;
/* 事件集合,每一 bit 表示 1 个事件,bit 位的值可以标记某事件是否发生 */
rt_uint32_t set;
;
/* rt_event_t 是指向事件结构体的指针类型 */
typedef struct rt_event* rt_event_t;
rt_event
对象从 rt_ipc_object
中派生,由 IPC容器管理。结构体 rt_ipc_object
定义如下:struct rt_objectcharname[RT_NAME_MAX];
/* 内核对象名称 */
rt_uint8_t type;
/* 内核对象类型 */
rt_uint8_t flag;
/* 内核对象的参数 */#ifdef RT_USING_MODULE
void*module_id;
/* 应用程序模块 ID */
#endif
rt_list_tlist;
/* 内核对象管理链表 */
;
struct rt_ipc_objectstruct rt_object parent;
/* 继承自 rt_object */
rt_list_tsuspend_thread;
/* 挂起的线程链表 */
;
结构体定义中,继承关系一目了然,不再赘述。下边看看如何对一个事件集进行操作。
管理事件集
事件集相关的操作函数如下图所示,主要包含:创建/初始化事件集、发送事件、接收事件、删除/脱离事件集。
文章图片
本文只介绍常用的几种系统函数。
1. 创建事件集
同信号量类似。RT-Thread 事件集创建也有两种方式:动态创建、静态初始化。
动态创建一个事件集的函数接口如下,调用这个函数创建一个事件集时,内核首先创建一个事件集控制块,然后对其进行基本的初始化。
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。
调用这个函数接口时,系统会从对象管理器中分配一个事件集对象,并对其初始化;然后初始化父类
IPC
对象。静态创建事件集有两步:(1)定义一个事件控制块结构体变量(2)调用函数对结构体变量初始化。
对事件集控制块变量初始化的函数为:
rt_err_t rt_event_init(rt_event_t event, const char* name, rt_uint8_t flag)
该函数对
event
指向的事件集控制块初始化,然后将其加入到系统对象容器中进行管理。创建事件集的标志变量取值有两种:
RT_IPC_FLAG_FIFO
,等待事件集的线程按照先进先出的方式进行排列。RT_IPC_FLAG_PRIO
,等待事件集的线程按照优先级的方式进行排列。
RT-Thread 提供的发送事件函数,可以一次性发送事件集中的一个或多个事件,函数原型如下:
rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set);
使用该函数接口时,通过参数
set
指定的事件标志来设定 event
事件集对象的事件标志值。然后遍历等待在 event
事件集对象上的等待线程链表,判断是否有线程的事件激活要求与当前 event
对象事件标志值匹配,如果有,则唤醒该线程。3. 接收事件
一个事件集对象可以同时等待接收多个事件,内核有两种方式唤醒等待事件集的线程:
- 逻辑与,表示只有当所有等待的事件都发生时才激活线程
- 逻辑或,只要有一个等待的事件发生就激活线程。
rt_err_t rt_event_recv(rt_event_tevent,
rt_uint32_tset,
rt_uint8_toption,
rt_int32_ttimeout,
rt_uint32_t *recved)
调用这个函数时,系统首先根据
set
参数和接收选项 option
来判断它要接收的事件是否发生了。函数的各个参数解释如下:
参数 | 描述 |
---|---|
event | 事件集控制块指针 |
set | 接收线程等待的事件 |
option | 接收选项 |
timeout | 等待事件的超时时间,单位 系统时钟节拍 |
recved | 指向收到的事件 |
RT_EOK
;超时,返回 -RT_ETIMEOUT
;出错,则返回 -RT_ERROR
。参数
option
的取值如下:/* 选择逻辑与或逻辑或的方式接收事件 */
#define RT_EVENT_FLAG_AND0x01/* 逻辑与 */
#define RT_EVENT_FLAG_OR0x02/* 逻辑或 *//* 选择清除重置事件标志位 */
#define RT_EVENT_FLAG_CLEAR0x04/* 清除事件标志位 */
当调用
rt_event_recv()
函数时,系统首先根据 set
参数和接收选项 option
来判断它要接收的事件是否发生:- 事件已经发生,则根据参数
option
上是否设置有RT_EVENT_FLAG_CLEAR
来决定是否重置事件的相对应的标志位。将发送的事件标志位填充到recved
指向的标志变量中。 - 事件没有发生,把等待的
set
和option
参数填入线程控制块结构中,然后线程进入到挂起状态。直到等待的事件满足条件或等待时间超过指定的时间。
-RT_ETIMEOUT
。实战演练
举例来说明事件集操作函数的用法,代码如下。创建两个线程,一个线程等待事件,一个线程发送事件。
#include <
rtthread.h>
#define THREAD_PRIORITY 8
#define THREAD_TIMESLICE 5#define EVENT_FLAG3 (1 <
<
3)
#define EVENT_FLAG5 (1 <
<
5)/* 事 件 控 制 块 */
static struct rt_event event;
static void rt_thread1_entry(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_kprintf("thread1 leave.\\n");
static void rt_thread2_entry(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 main()/* 线程控制块指针 */
rt_thread_t thread1 = RT_NULL;
rt_thread_t thread2 = RT_NULL;
rt_err_t result;
/* 初始化事件对象 */
result = rt_event_init(&
event, "event", RT_IPC_FLAG_FIFO);
if (result != RT_EOK)rt_kprintf("init event failed.\\n");
return -1;
/* 动态创建线程1 */
thread1 = rt_thread_create("thread1", rt_thread1_entry, RT_NULL,
1024, THREAD_PRIORITY - 1, THREAD_TIMESLICE);
if(thread1 != RT_NULL)/* 启动线程 */
rt_thread_startup(thread1);
/* 动态创建线程2 */
thread2 = rt_thread_create("thread2", rt_thread2_entry, RT_NULL,
1024, THREAD_PRIORITY, THREAD_TIMESLICE);
if(thread2 != RT_NULL)/* 启动线程 */
rt_thread_startup(thread2);
编译,运行结果如下:
文章图片
其他操作函数
对于 RT-Thread 事件集操作来说,还有删除事件集函数没有介绍。可以简单了解一下。
1. 删除动态创建的事件集
删除由
rt_event_create()
函数创建的事件集,可以调用如下函数:rt_err_t rt_event_delete(rt_event_t event);
调用此函数,可以释放事件集控制块占用的内存资源。在删除一个事件集对象时,应该确保该事件集不再被使用。
在删除前会唤醒所有挂起在该事件集上的线程,然后释放事件集对象占用的内存块。
2. 脱离静态创建的事件集
删除
rt_event_init()
初始化的事件集,可以用如下函数:rt_err_t rt_event_detach(rt_event_t event)
调用此函数时,首先会唤醒所有挂起在该事件集等待队列上的线程,然后将该事件集从内核对象管理器中脱离。
小结
至此,RT-Thread 中用于线程间同步的三种方式,全部完毕。
- 信号量(包含计数信号量、二值信号量)
- 互斥量(互斥信号量)
- 事件集(事件标志组)
因此,学习一项新知识点,尽量掌握它的本质原理,争取做到触类旁通。
OK,今天先到这,下次继续。加油~
推荐阅读
- Kubernetes EFK
- #yyds干货盘点#Zabbi学习
- 为什么MySQL的主键查询这么快()
- #yyds干货盘点#ffmpeg
- #yyds干货盘点#java高级用法之:在JNA中将本地方法映射到JAVA代码中
- c++核心编程--函数的重载
- k8s集群Job负载支持多个Pod可靠并发,如何权衡利弊选择适合的并行计算模式()
- #yyds干货盘点#IDEA上运行Flink任务
- C语言文件操作