用邮箱实现多事件的单向同步

用邮箱实现多事件的单向同步

概述

信号量通常只能实现单一事件或资源的同步,多个事件的单向同步可以使用队列或事件组。

上节讲述了使用事件组同步多个事件,虽然可以通过事件组传递多个事件,并且接收方可以同步不同的 bits 位知道数据的发送方,但事件组还未能解决下述问题:

1)事件组无法解决事件堆积问题。重复触发的事件无法被计数,可能存在丢失。

2)事件组无法对事件的详细信息进行说明。就好比女朋友通知你去一起吃饭,但告诉你“吃随意”一样,难受!

消息队列,也称邮箱,可以用于解决上述问题,本节重点介绍消息队列。

队列的基本模型

队列的简化模型如入下图所示,从此图可知:

在这里插入图片描述

  • 队列包含一个数据缓冲区,该缓冲区中可以包含若干块数据,每块数据区域可被称为"条目"(item)。

  • 每个条目的最大大小固定。队列可以传输变长的数据,最大大小不能超过条目的最大大小(传递指针也是可以的,但指针指定的内存空间必须保证在使用前有效)。

  • 创建队列时就要指定条目个数、每个条目数据的最大大小

  • 数据的操作通过读索引、写索引采用先进先出的方法(FIFO,First In First Out):写数据时放到尾部,读数据时从头部读。

  • 也可以强制将数据放到队列头部,则下一个要读的数据就是它。

  • 使用队列中的数据后,可以删除该项条目,也可以保留该项条目。

  • 允许延时发送。即发送数据时,若队列已经满了(没有多余空间存放数据了),可以通过指定延时时间,延时发送该条信息。

队列的不足:

1)消耗的内存空间当然比信号量、事件组要大一些。

2)具备一定的缓存能力,但若发送者的速率过快,而队列的大小没有足够大,则还是存在溢出风险。

队列的操作

队列的基本操作是:创建、发送(send)、接收(接收)。其中发送可以选择发送到队列后(正常排队),或者发送到队列前(直接成为下一个要被接收的消息)。

在这里插入图片描述

其中涉及的 API 主要有以下几个,每个 API 的相关信息都标识在注释中。

/*非0:成功,返回队列句柄,以后使用句柄来操作队列
 *NULL:失败,因为内存不足
*/
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, // 队列长度,最多能存放多少个条目(item)
							UBaseType_t uxItemSize ); // 每个条目(item)的大小:以字节为单位

/* pxQueue : 复位哪个队列;
 * 返回值: pdPASS(必定成功)
 */
BaseType_t xQueueReset( QueueHandle_t pxQueue);

/* 等同于xQueueSendToBack
 * 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
 * 返回值: pdPASS:数据成功写入了队列,errQUEUE_FULL:写入失败,因为队列满了。
 */
BaseType_t xQueueSend(
                                QueueHandle_t    xQueue,// 队列句柄,要写哪个队列
                                const void       *pvItemToQueue, // 数据指针,这个数据的值会被复制进队列,
                                TickType_t       xTicksToWait /* 如果队列满则无法写入新数据,可以让任务进入阻塞状态,xTicksToWait表示阻塞的最大时间(Tick Count)。如果被设为0,无法写入数据时函数会立刻返回;如果被设为portMAX_DELAY,则会一直阻塞直到有空间可写*/
                            );

/* 
 * 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
 */
BaseType_t xQueueSendToBack(
                                QueueHandle_t    xQueue,
                                const void       *pvItemToQueue,
                                TickType_t       xTicksToWait
                            );

/* 
 * 往队列头部写入数据,如果没有空间,阻塞时间为xTicksToWait
 */
BaseType_t xQueueSendToFront(
                                QueueHandle_t    xQueue,
                                const void       *pvItemToQueue,
                                TickType_t       xTicksToWait
                            );

/* 
 * 仅适用于长度为1的队列,在队列上写入数据。如果队列已满,则覆盖队列中保存的值。
 */
xQueueOverwrite(xQueue, 
				pvItemToQueue)/* 从队列中接收项目,并从队列中移除对应的条目
 * pdPASS:从队列读出数据入
 * errQUEUE_EMPTY:读取失败,因为队列空了。
 */
BaseType_t xQueueReceive( QueueHandle_t xQueue, // 队列句柄,要读哪个队列
                          void * const pvBuffer, // bufer指针,队列的数据会被复制到这个buffer
                          TickType_t xTicksToWait ); // 果队列空则无法读出数据,可以让任务进入阻塞状态

/* 从队列中接收项目而不从队列中移除对应的条目。
 * pdPASS:从队列读出数据入
 * errQUEUE_EMPTY:读取失败,因为队列空了。
 */
BaseType_t xQueuePeek(QueueHandle_t xQueue, // 队列句柄,要读哪个队列
                      void *const pvBuffer, // bufer指针,队列的数据会被复制到这个buffer
                      TickType_t xTicksToWait); // 果队列空则无法读出数据,可以让任务进入阻塞状态

注意:发送到队列的消息是通过拷贝(Copy, 复制)方式实现的,这意味着队列存储的数据是原数据的备份,而不是原数据的引用(局部变量的值可以发送到队列中,后续即使函数退出、局部变量被回收,也不会影响队列中的数据)。

队列的等待-通知机制

此外,队列同样实现了类似前述二值信号量的等待-通知机制(也称为pend-post机制),即无消息时,任务可以暂停运行,进入延时等待消息的状态,有消息时可以立即通知相关的任务唤醒运行。

但是不同于前述信号量的简单同步,队列可能被用于多个任务之间的同步,因此需要考虑以下场景:

1)多个任务等待消息:

某个任务读队列时,如果队列没有数据,则该任务可以进入阻塞状态:还可以指定阻塞的时间。如果队列有数据了,则该阻塞的任务会变为就绪态。如果一直都没有数据,则时间到之后它也会进入就绪态。

既然读取队列的任务个数没有限制,那么当多个任务读取空队列时,这些任务都会进入阻塞状态:有多个任务在等待同一个队列的数据。当队列中有数据时,哪个任务会进入就绪态?

  • 优先级最高的任务

  • 如果大家的优先级相同,那等待时间最久的任务会进入就绪态

2)多个任务发消息:

跟读队列类似,一个任务要写队列时,如果队列满了,该任务也可以进入阻塞状态:还可以指定阻塞的时间。如果队列有空间了,则该阻塞的任务会变为就绪态。如果一直都没有空间,则时间到之后它也会进入就绪态。

既然写队列的任务个数没有限制,那么当多个任务写"满队列"时,这些任务都会进入阻塞状态:有多个任务在等待同一个队列的空间。当队列中有空间时,哪个任务会进入就绪态?

  • 优先级最高的任务

  • 如果大家的优先级相同,那等待时间最久的任务会进入就绪态。

需求及功能解析

如前所述,在一些情况下,我们仅仅接收到通知还不够,往往还需要知道下述详细的信息来指导下一步的动作:

1)是谁在发送通知

只是采取信号量往往无法实现。

这种情况的需求:某一个任务等待多个事件中任意一个事件的发生。

2)发送通知的目的或者详情是什么

示例中通过定义一个枚举,表示发送者是谁,同时定义一个动作类型,即加 1,减1。来演示这种情况。

其本质在于,队列不仅可以通知,还可以附带一些数据。

示例解析

示例的 log 给出了,task3 收到 task1\task2 的消息,并打印消息的来源、以及具体指示的操作。

Hello world!
This is esp32 chip with 2 CPU core(s), WiFi/BT/BLE, Minimum free heap size: 294416 bytes
received from flag1, event is Add, count:1
received from task2, event is Sub, count:0
received from task2, event is Sub, count:-1
received from flag1, event is Add, count:0
received from flag1, event is Add, count:1
received from task2, event is Sub, count:0

讨论

消息队列使用注意事项

在使用FreeRTOS提供的消息队列函数的时候,需要了解以下几点:

  • 队列读取采用的是先进先出(FIFO)模式,会先读取先存储在队列中的数据。当然也FreeRTOS也支持后进先出(LIFO)模式,那么读取的时候就会读取到后进队列的数据。

  • 在获取(Recv)队列中的消息时候,必须要定义一个存储读取数据的地方,并且该数据区域大小不小于消息大小,否则,很可能引发地址非法的错误。

  • 无论是发送或者是接收消息都是以值拷贝的方式进行,如果消息过于庞大,可以将消息的地址(指针)作为消息进行发送、接收;同时要注意该指针指向的内存空间要在接收消息时仍处于有效状态,否则可能出现非法引用内存空间的错误。

  • 队列是具有自己独立权限的内核对象,并不属于任何任务。通过队列可以实现多个任务向同一队列写入和读出。但是实际中多是一个任务从队列中读取,较少出现多个任务从一个队列读取的情况。

总结

1)相比信号量、事件组。消息队列(也称为邮箱)可以用于多任务的同步,并提供一定的消息缓存、说明消息详细信息的能力。

2)队列的基本操作是:创建、发送(send)、接收(接收)。其中发送可以选择发送到队列后(正常排队),或者发送到队列前(直接成为下一个要被接收的消息)。

3)队列读取通常采用的是先进先出(FIFO)模式,会先读取先存储在队列中的数据。也支持后进先出(LIFO)模式,那么读取的时候就会读取到后进队列的数据。

4)队列提供的等待-通知机制,在同步多任务出现时,依据任务优先级、等待时间进行裁决分配。

资源链接

1)Learning-FreeRTOS-with-esp32 系列博客介绍
2)对应示例的 code 链接 (点击直达代码仓库)

3)下一篇:使用信号量实现简单双向同步

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

物联网老王

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值