事件

事件主要用于线程间的同步,与信号量不同,它的特点是可以实现一对多,多对多的同步。即一个线程可等待多个事件的触发:可以是其中任意一个事件唤醒线程进行事件处理的操作;也可以是几个事件都到达后才唤醒线程进行后续的处理;同样,事件也可以是多个线程同步多个事件,这种多个事件的集合可以用一个32位无符号整型变量来表示,变量的每一位代表一个事件,线程通过“逻辑与”或“逻辑或”与一个或多个事件建立关联,形成一个事件集。事件的“逻辑或”也称为是独立型同步,指的是线程与任何事件之一发生同步;事件“逻辑与”也称为是关联型同步,指的是线程与若干事件都发生同步。

RT-Thread定义的事件有以下特点:

  • 事件只与线程相关,事件间相互独立:每个线程拥有32个事件标志,采用一个32 bit无符号整型数进行记录,每一个bit代表一个事件。若干个事件构成一个事件集;
  • 事件仅用于同步,不提供数据传输功能;
  • 事件无排队性,即多次向线程发送同一事件(如果线程还未来得及读走),其效果等同于只发送一次。
    在RT-Thread实现中,每个线程都拥有一个事件信息标记,它有三个属性,分别是RT_EVENT_FLAG_AND(逻辑与),RT_EVENT_FLAG_OR(逻辑或)以及RT_EVENT_FLAG_CLEAR (清除标记)。当线程等待事件同步时,可以通过32个事件标志和这个事件信息标记来判断当前接收的事件是否满足同步条件。

事件工作示意图

如图 事件工作示意图 所示,线程#1的事件标志中第2位和第29位被置位,如果事件信息标记位设为逻辑与,则表示线程#1只有在事件1和事件29都发生以后才会被触发唤醒,如果事件信息标记位设为逻辑或,则事件1或事件29中的任意一个发生都会触发唤醒线程#1。如果信息标记同时设置了清除标记位,则当线程#1唤醒后将主动把事件1和事件29清为零,否则事件标志将依然存在(即置1)。

事件控制块

  1. struct rt_event
  2. {
  3. struct rt_ipc_object parent; /* 继承自ipc_object类 */
  4. rt_uint32_t set; /* 事件集合 */
  5. };
  6. /* rt_event_t是指向事件结构体的指针 */
  7. typedef struct rt_event* rt_event_t;

rt_event对象从rt_ipc_object 中派生,由IPC容器管理。

事件相关接口

创建事件

当创建一个事件时,内核首先创建一个事件控制块,然后对该事件控制块进行基本的初始化,创建事件使用下面的函数接口:

  1. rt_event_t rt_event_create (const char* name, rt_uint8_t flag);

调用该函数接口时,系统会从动态内存堆中分配事件对象,然后进行对象的初始化,IPC对象初始化,并把set设置成0。

函数参数


  1. 参数 描述

  1. name 事件的名称;
  2.  
  3. flag 事件的标志,可以使用如下的数值:

  1. #define RT_IPC_FLAG_FIFO 0x00 /* IPC参数采用FIFO方式*/
  2. #define RT_IPC_FLAG_PRIO 0x01 /* IPC参数采用优先级方式*/

函数返回

创建成功返回事件对象的句柄;创建失败返回RT_NULL。

删除事件

系统不再使用事件对象时,通过删除事件对象控制块来释放系统资源。删除事件可以使用下面的函数接口:

  1. rt_err_t rt_event_delete (rt_event_t event);

在调用rt_event_delete函数删除一个事件对象时,应该确保该事件不再被使用。在删除前会唤醒所有挂起在该事件上的线程(线程的返回值是-RT_ERROR),然后释放事件对象占用的内存块。

函数参数


  1. 参数 描述

  1. event 事件对象的句柄。

函数返回

RT_EOK

初始化事件

静态事件对象的内存是在系统编译时由编译器分配的,一般放于数据段或ZI段中。在使用静态事件对象前,需要先行对它进行初始化操作。初始化事件使用下面的函数接口:

  1. rt_err_t rt_event_init(rt_event_t event, const char* name, rt_uint8_t flag);

调用该接口时,需指定静态事件对象的句柄(即指向事件控制块的指针),然后系统会初始化事件对象,并加入到系统对象容器中进行管理。

函数参数


  1. 参数 描述

  1. event 事件对象的句柄。
  2.  
  3. name 事件名称;
  4.  
  5. flag 事件的标志,可以使用如下的数值:

  1. #define RT_IPC_FLAG_FIFO 0x00 /* IPC参数采用FIFO方式*/
  2. #define RT_IPC_FLAG_PRIO 0x01 /* IPC参数采用优先级方式*/

函数返回

RT_EOK

脱离事件

脱离事件是将事件对象从内核对象管理器中删除。脱离事件使用下面的函数接口:

  1. rt_err_t rt_event_detach(rt_event_t event);

用户调用这个函数时,系统首先唤醒所有挂在该事件等待队列上的线程(线程的返回值是- RT_ERROR ),然后将该事件从内核对象管理器中删除。

函数参数


  1. 参数 描述

  1. event 事件对象的句柄。

函数返回

RT_EOK

接收事件

内核使用32位的无符号整型数来标识事件,它的每一位代表一个事件,因此一个事件对象可同时等待接收32个事件,内核可以通过指定选择参数“逻辑与”或“逻辑或”来选择如何激活线程,使用“逻辑与”参数表示只有当所有等待的事件都发生时才激活线程,而使用“逻辑或”参数则表示只要有一个等待的事件发生就激活线程。接收事件使用下面的函数接口:

  1. rt_err_t rt_event_recv(rt_event_t event, rt_uint32_t set, rt_uint8_t option,
  2. rt_int32_t timeout, rt_uint32_t* recved);

当用户调用这个接口时,系统首先根据set参数和接收选项来判断它要接收的事件是否发生,如果已经发生,则根据参数option上是否设置有RT_EVENT_FLAG_CLEAR来决定是否重置事件的相应标志位,然后返回(其中recved参数返回收到的事件);如果没有发生,则把等待的set和option参数填入线程本身的结构中,然后把线程挂起在此事件对象上,直到其等待的事件满足条件或等待时间超过指定的超时时间。如果超时时间设置为零,则表示当线程要接受的事件没有满足其要求时就不等待,而直接返回-RT_ETIMEOUT。

函数参数


  1. 参数 描述

  1. event 事件对象的句柄。
  2.  
  3. set 接收线程感兴趣的事件;
  4.  
  5. option 接收选项;
  6.  
  7. timeout 指定超时时间;
  8.  
  9. recved 指向收到的事件;

函数返回

正确接收返回RT_EOK,超时返回-RT_ETIMEOUT,其他返回-RT_ERROR。

发送事件

通过发送事件服务,可以发送一个或多个事件。发送事件可以使用下面的函数接口:

  1. rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set);

使用该函数接口时,通过参数set指定的事件标志来设定event对象的事件标志值,然后遍历等待在event事件对象上的等待线程链表,判断是否有线程的事件激活要求与当前event对象事件标志值匹配,如果有,则唤醒该线程。

函数参数


  1. 参数 描述

  1. event 事件对象的句柄。
  2.  
  3. set 发送的事件集;

函数返回

RT_EOK

使用事件的例程如下例所示:

  1. /*
  2. * 程序清单:事件例程
  3. *
  4. * 这个程序会创建3个动态线程及初始化一个静态事件对象
  5. * 一个线程等待在事件对象上以接收事件;
  6. * 一个线程定时发送事件 (事件3)
  7. * 一个线程定时发送事件 (事件5)
  8. */
  9. #include <rtthread.h>
  10. #include "tc_comm.h"
  11.  
  12. /* 指向线程控制块的指针 */
  13. static rt_thread_t tid1 = RT_NULL;
  14. static rt_thread_t tid2 = RT_NULL;
  15. static rt_thread_t tid3 = RT_NULL;
  16.  
  17. /* 事件控制块 */
  18. static struct rt_event event;
  19.  
  20. /* 线程1入口函数 */
  21. static void thread1_entry(void *param)
  22. {
  23. rt_uint32_t e;
  24.  
  25. while (1)
  26. {
  27. /* 以逻辑与的方式接收事件 */
  28. if (rt_event_recv(&event, ((1 << 3) | (1 << 5)),
  29. RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR,
  30. RT_WAITING_FOREVER, &e) == RT_EOK)
  31. {
  32. rt_kprintf("thread1: AND recv event 0x%x\n", e);
  33. }
  34.  
  35. rt_kprintf("thread1: delay 1s to prepare second event\n");
  36. rt_thread_delay(10);
  37.  
  38. /* 以逻辑或的方式接收事件 */
  39. if (rt_event_recv(&event, ((1 << 3) | (1 << 5)),
  40. RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR,
  41. RT_WAITING_FOREVER, &e) == RT_EOK)
  42. {
  43. rt_kprintf("thread1: OR recv event 0x%x\n", e);
  44. }
  45.  
  46. rt_thread_delay(5);
  47. }
  48. }
  49.  
  50. /* 线程2入口函数 */
  51. static void thread2_entry(void *param)
  52. {
  53. /* 线程2持续地发送事件#3 */
  54. while (1)
  55. {
  56. rt_kprintf("thread2: send event1\n");
  57. rt_event_send(&event, (1 << 3));
  58.  
  59. rt_thread_delay(10);
  60. }
  61. }
  62.  
  63. /* 线程3入口函数 */
  64. static void thread3_entry(void *param)
  65. {
  66. /* 线程3持续地发送事件#5 */
  67. while (1)
  68. {
  69. rt_kprintf("thread3: send event2\n");
  70. rt_event_send(&event, (1 << 5));
  71.  
  72. rt_thread_delay(20);
  73. }
  74. }
  75.  
  76. int event_simple_init()
  77. {
  78. /* 初始化事件对象 */
  79. rt_event_init(&event, "event", RT_IPC_FLAG_FIFO);
  80.  
  81. /* 创建线程1 */
  82. tid1 = rt_thread_create("t1",
  83. thread1_entry, /* 线程入口是thread1_entry */
  84. RT_NULL, /* 入口参数是RT_NULL */
  85. THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE);
  86. if (tid1 != RT_NULL)
  87. rt_thread_startup(tid1);
  88. else
  89. tc_stat(TC_STAT_END | TC_STAT_FAILED);
  90.  
  91. /* 创建线程2 */
  92. tid2 = rt_thread_create("t2",
  93. thread2_entry, /* 线程入口是thread2_entry */
  94. RT_NULL, /* 入口参数是RT_NULL */
  95. THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE);
  96. if (tid2 != RT_NULL)
  97. rt_thread_startup(tid2);
  98. else
  99. tc_stat(TC_STAT_END | TC_STAT_FAILED);
  100.  
  101. /* 创建线程3 */
  102. tid3 = rt_thread_create("t3",
  103. thread3_entry, /* 线程入口是thread3_entry */
  104. RT_NULL, /* 入口参数是RT_NULL */
  105. THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE);
  106. if (tid3 != RT_NULL)
  107. rt_thread_startup(tid3);
  108. else
  109. tc_stat(TC_STAT_END | TC_STAT_FAILED);
  110.  
  111. return 0;
  112. }
  113.  
  114. #ifdef RT_USING_TC
  115. static void _tc_cleanup()
  116. {
  117. /* 调度器上锁,上锁后,将不再切换到其他线程,仅响应中断 */
  118. rt_enter_critical();
  119.  
  120. /* 删除线程 */
  121. if (tid1 != RT_NULL && tid1->stat != RT_THREAD_CLOSE)
  122. rt_thread_delete(tid1);
  123. if (tid2 != RT_NULL && tid2->stat != RT_THREAD_CLOSE)
  124. rt_thread_delete(tid2);
  125. if (tid3 != RT_NULL && tid3->stat != RT_THREAD_CLOSE)
  126. rt_thread_delete(tid3);
  127.  
  128. /* 执行事件对象脱离 */
  129. rt_event_detach(&event);
  130.  
  131. /* 调度器解锁 */
  132. rt_exit_critical();
  133.  
  134. /* 设置TestCase状态 */
  135. tc_done(TC_STAT_PASSED);
  136. }
  137.  
  138. int _tc_event_simple()
  139. {
  140. /* 设置TestCase清理回调函数 */
  141. tc_cleanup(_tc_cleanup);
  142. event_simple_init();
  143.  
  144. /* 返回TestCase运行的最长时间 */
  145. return 100;
  146. }
  147. /* 输出函数命令到finsh shell中 */
  148. FINSH_FUNCTION_EXPORT(_tc_event_simple, a simple event example);
  149. #else
  150. /* 用户应用入口 */
  151. int rt_application_init()
  152. {
  153. event_simple_init();
  154.  
  155. return 0;
  156. }
  157. #endif

使用场合

事件可使用于多种场合,它能够在一定程度上替代信号量,用于线程间同步。一个线程或中断服务例程发送一个事件给事件对象,而后等待的线程被唤醒并对相应的事件进行处理。但是它与信号量不同的是,事件的发送操作在事件未清除前,是不可累计的,而信号量的释放动作是累计的。事件另外一个特性是,接收线程可等待多种事件,即多个事件对应一个线程或多个线程。同时按照线程等待的参数,可选择是“逻辑或”触发还是“逻辑与”触发。这个特性也是信号量等所不具备的,信号量只能识别单一的释放动作,而不能同时等待多种类型的释放。如图 多事件接收 所示:

多事件接收

各个事件类型可分别发送或一起发送给事件对象,而事件对象可以等待多个线程,它们仅对它们感兴趣的事件进行关注。当有它们感兴趣的事件发生时,线程就将被唤醒并进行后续的处理动作。