事件集也是线程间同步的机制之一,一个事件集可以包含多个事件,利用事件集可以完成一对多,多对多的线程间同步。
下面以坐公交为例说明事件,在公交站等公交时可能有以下几种情况:
①P1 坐公交去某地,只有一种公交可以到达目的地,等到此公交即可出发。
②P1 坐公交去某地,有 3 种公交都可以到达目的地,等到其中任意一辆即可出发。
③P1 约另一人 P2 一起去某地,则 P1 必须要等到 “同伴 P2 到达公交站” 与“公交到达公交站”两个条件都满足后,才能出发。
这里,可以将 P1 去某地视为线程,将 “公交到达公交站”、“同伴 P2 到达公交站” 视为事件的发生,情况①是特定事件唤醒线程;情况②是任意单个事件唤醒线程;情况③是多个事件同时发生才唤醒线程。
事件集主要用于线程间的同步,与信号量不同,它的特点是可以实现一对多,多对多的同步。
即一个线程与多个事件的关系可设置为:其中任意一个事件唤醒线程,或几个事件都到达后才唤醒线程进行后续的处理。
同样,事件也可以是多个线程同步多个事件。
这种多个事件的集合可以用一个32位的无符号整型变量来表示,变量的每一位代表一个事件,线程通过“逻辑与”或“逻辑或”将一个或多个事件关联起来,形成事件组合。
事件的 “逻辑或” 也称为是独立型同步,指的是线程与任何事件之一发生同步;事件 “逻辑与” 也称为是关联型同步,指的是线程与若干事件都发生同步。
RT-Thread定义的事件集有以下特点:
每个线程都拥有一个事件信息标记,它有三个属性,分别是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)。
在RT-Thread中,事件集控制块是操作系统用于管理事件的一个数据结构,由结构体struct rt_event表示。
struct rt_event
{
struct rt_ipc_object parent;
/* 事件集合,每一bit表示1个事件,bit位的值可以标记事件是否发生 */
rt_uint32_t set;
};
typedef struct rt_event* rt_event_t;
当创建一个事件集时,内核首先创建一个事件集控制块,然后对该事件集控制块进行基本的初始化,创建事件集使用下面的函数接口:
rt_event_t rt_event_create(const char*name, rt_uint8_t flag);
调用该函数接口时,系统会从对象管理器中分配事件集对象,并初始化这个对象,然后初始化父类IPC对象。
发送事件函数可以发送事件集中的一个或多个事件,如下:
rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set);
使用该函数接口时,通过参数set指定的事件标志来设定event事件集对象的事件标志值,然后遍历等待在event事件集对象上的等待线程链表,判断是否有线程的事件激活要求与当前event对象事件标志值匹配,如果有,则唤醒线程。
内核使用32位的无符号整数来标识事件集,它的每一位代表一个事件,因此一个事件集对象可同时等待接收32个事件,内核可以通过指定选择参数“逻辑与”或“逻辑或”来选择如何激活线程,使用 “逻辑与” 参数表示只有当所有等待的事件都发生时才激活线程,而使用 “逻辑或” 参数则表示只要有一个等待的事件发生就激活线程。接收事件使用下面的函数接口:
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上是否设置是否重置事件的相应标志位,然后返回(其中recved参数返回接收到的事件);如果没有发生,则把等待的 set 和 option 参数填入线程本身的结构中,然后把线程挂起在此事件上,直到其等待的事件满足条件或等待时间超过指定的超时时间。如果超时时间设置为零,则表示当线程要接受的事件没有满足其要求时就不等待,而直接返回 - RT_ETIMEOUT。
事件集可适用于多种场合,它能够在一定程度上替代信号量,用于线程间同步。
一个线程或中断服务例程发送一个事件给事件集对象,而后等待的线程被唤醒并对相应的事件进行处理。
但是它与信号量不同的是,事件的发送操作在事件未清除前,是不可累计的,而信号量的释放动作是累计的。
事件的另一个特性是,接收线程可等待多种事件,即多个事件对应一个线程或多个线程。
同时按照线程等待的参数,可选择是 “逻辑或” 触发还是 “逻辑与” 触发。这个特性也是信号量等所不具备的,信号量只能识别单一的释放动作,而不能同时等待多种类型的释放。如下图所示为多事件接收示意图:
一个事件集中包含 32 个事件,特定线程只等待、接收它关注的事件。可以是一个线程等待多个事件的到来(线程 1、2 均等待多个事件,事件间可以使用 “与” 或者 “或” 逻辑触发线程),也可以是多个线程等待一个事件的到来(事件 25)。当有它们关注的事件发生时,线程将被唤醒并进行后续的处理动作。