RTX 线程通信之——内存池
Posted Albert Nie
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RTX 线程通信之——内存池相关的知识,希望对你有一定的参考价值。
Memory Pool
内存池 (Memory Pool) 同消息队列一样,可以通过它实现线程间的数据传输。
为什么需要内存池?
有人说,有了消息队列,咱还要设计一个内存池干嘛,吃饱了没事干啊~,别说,还真不是!
确实,我们可以设计消息队列(Message Queue)来进行线程间的大量数据传输,从而实现线程间通信(Inter-Thread-Communication)。但别忘了,消息队列存在以下缺点:
- 开销大。每次通信都需要在消息队列中频繁移动数据,即FIFO式的移动
- 消息队列只能传输整型或指针类型的数据,灵活性不高
针对上述问题,一种解决方案是创建一块静态内存池 (也就是共享内存池),用来存放传输的数据,然后将该内存池的地址通过消息队列传输,从而实现“零移动”的数据传输。换句话说,数据本身没有移动,移动的是数据对象的地址。内存池的优点如下:
- 开销小。不需要移动数据本身,只需要移动数据对象地址,便可实现线程通信。
- 内存池可以存放复杂类型的对象,灵活度较高
综上,内存池很好地弥补了消息队列的不足。当然,要实现数据通信,一般还得【内存池+消息队列】。为什么是一般呢?
有什么场合是只能用消息队列的吗?别说,还真有,使用内存池的一大前提是两个线程之间有共享内存,换句话说就是有相同的地址空间。如果没有怎么办?比如临界区,一次只能允许一个线程访问,这个时候内存池不能用了,而消息队列就可以派上用场了~
什么是内存池?
啥是内存池?咱先来看看官方对内存池的介绍:
Memory Pools are fixed-size blocks of memory that are thread-safe. They operate much faster than the dynamically allocated heap and do not suffer from fragmentation. Being thread-safe, they can be accessed from threads and ISRs alike. A Memory Pool can be seen as a linked list of available (unused) memory blocks of fixed and equal size. Allocating memory from a pool (using osMemoryPoolAlloc) simply unchains a block from the list and hands over control to the user. Freeing memory to the pool (using osMemoryPoolFree) simply rechains the block into the list.
抓重点,内存池有以下特点:
- 内存池由一系列固定大小的未使用的内存块(memory blocks)组成,可以看成一个动态链表,结点就是内存块。之所以是动态的,是因为在分配和释放的过程中该链表的长度会动态变化。
- 内存池是线程安全的,其操作速度也比通常的动态内存分配(堆区分配)更快,也不会造成内存碎片。说它是线程安全的,指的是其可以正常被多个线程或ISR访问,而不会出现错误行为。
共享内存是线程之间数据通信的一种基本模型,而内存池正是采用了这一思想,即获得地址即可访问数据!
RTX内存池API
要使用内存池,得先知道RTX为用户提供的相关定义和函数接口,下面作简要介绍~
类型
osMemoryPoolAttr_t
: 内存池属性结构体osMemoryPoolId_t
: 内存池句柄
函数
osMemoryPoolNew
osMemoryPoolId_t osMemoryPoolNew(uint32_t block_count, uint32_t block_size,const osMemoryPoolAttr_t* attr)
// 输入
* block_count : 内存池中内存块的最大个数
* block_size : 每个内存块的字节数
* attr : 内存池属性,默认为NULL
// 输出
* 内存池ID
// 注意
* 不能在ISR中调用该函数
osMemoryPoolGetName
const char* osMemoryPoolGetName(osMemoryPoolId_t mp_id)
// 输入
* mp_id : 内存池ID
// 输出
* 内存池名字
osMemoryPoolAlloc
void* osMemoryPoolAlloc(osMemoryPoolId_t mp_id,uint32_t timeout)
// 输入
* mp_id: 内存池ID
* timeout: 超时设置
// 输出
* 分配的内存块地址或者NULL
osMemoryPoolFree
osStatus_t osMemoryPoolFree(osMemoryPoolId_t mp_id,void* block)
// 输入
* mp_id : 内存池ID
* block : 待释放的内存块地址c
// 输出
* 函数执行后的状态码: osOK、osErrorParameter、osErrorResource
osMemoryPoolGetCapacity
uint32_t osMemoryPoolGetCapacity(osMemoryPoolId_t mp_id)
// 输入
* mp_id : 内存池ID
// 输出
* 内存池允许的最大内存块数量
osMemoryPoolGetBlockSize
uint32_t osMemoryPoolGetBlockSize(osMemoryPoolId_t mp_id)
// 输入
* mp_id : 内存池ID
// 输出
* 内存块的大小
osMemoryPoolGetCount
uint32_t osMemoryPoolGetCount(osMemoryPoolId_t mp_id)
// 输入
* mp_id : 内存池ID
// 输出
* 内存池中已经被使用的内存块个数
osMemoryPoolGetSpace
uint32_t osMemoryPoolGetSpace(osMemoryPoolId_t mp_id)
// 输入
* mp_id : 内存池ID
// 输出
* 内存池中可用的内存块个数
osMemoryPoolDelete
osStatus_t osMemoryPoolDelete(osMemoryPoolId_t mp_id)
// 输入
* mp_id :内存池ID
// 输出
* 函数执行后的状态码
// 注意
* 不能在ISR中调用该函数
案例: 按键控制LED灯
案例是最好的学习方式,这里笔者介绍一个玩具案例,具体步骤如下。
功能:实现两个线程,一个线程用来不断读取按键的状态,并将按键的状态封装到一个结构体中,然后通过内存池和消息队列将该数据发送给另一个线程;另一个线程负责接收该数据,并根据该数据来点亮对应的LED灯。
* KEY1 键按下后松开, 红灯闪烁。
* KEY2 键按下后松开, 绿灯闪烁。
定义相关
根据功能,我们需要定义两个线程,相应地有两个线程函数;还有一个内存池,包括相应的内存块数据结构体;最后还需要定义一个消息队列。这里需要说明一下通常我们是将内存池和消息队列结合起来使用。内存池负责保存复杂对象本身,然后通过将该对象的地址传入消息队列,实现线程间通信。
// 定义两个线程
osThreadId_t led; // LED灯
osThreadId_t key; // 按键
void thread_led(void* arg); // LED灯线程函数
void thread_key(void* arg); // 按键线程函数
osMemoryPoolId_t mp_id; // 内存池
typedef struct { // 内存块数据结构体
uint8_t red;
uint8_t green;
}memory_block_t;
osMessageQueueId_t mq_id; // 消息队列
创建相关
我们在主线程app_main
的线程函数中创建我们的线程,内存池,和消息队列,并执行一些按键和GPIO的初始化函数。
void app_main (void *arg) {
// 外设初始化
LED_GPIO_Config();
Key_GPIO_Config();
// 内存池 + 消息队列
mp_id = osMemoryPoolNew(16,sizeof(memory_block_t),NULL); // 内存池负责保存对象
mq_id = osMessageQueueNew(16,sizeof(memory_block_t*),NULL); // 消息队列负责传递对象的地址
// 两个线程
key = osThreadNew(thread_key,NULL,NULL);
led = osThreadNew(thread_led,NULL,NULL);
osThreadExit(); // 任务完成,退出
}
执行相关
我们的执行函数,当然是两个线程函数了。首先我们来看管理按键的线程函数。
void thread_key(void* arg)
{
memory_block_t* mb_led; // 内存块指针
while(1){
// 请求一个内存块
mb_led = (memory_block_t*)osMemoryPoolAlloc(mp_id,osWaitForever);
// 扫描按键,按键按下返回 1 ,否则0
if( Key_Scan(GPIOA,GPIO_Pin_0) == KEY_ON)
mb_led->red = 0; // 红灯亮
else
mb_led->red = 1; // 红灯灭
if(Key_Scan(GPIOC,GPIO_Pin_13) == KEY_ON)
mb_led->green = 0; // 绿灯亮
else
mb_led->green = 1; // 绿灯灭
// 将数据传入消息队列
osMessageQueuePut(mq_id,&mb_led,NULL,osWaitForever);
osDelay(100);
}
}
然后,再来看管理LED灯的线程函数。
void thread_led(void* arg)
{
memory_block_t* mb_led; // 内存块指针
while(1){
osMessageQueueGet(mq_id,&mb_led,NULL,osWaitForever);
LED1(mb_led->red);
LED2(mb_led->green);
// 完成一次传输,释放该内存块
osMemoryPoolFree(mp_id,mb_led);
}
}
具体的GPIO,按键配置啥的,就不讲了,那不是本文的范畴,你们没问题的~
实验效果
由于按下按键,松开后LED灯才会亮,所以为了抓拍这个实验现象,我拍了几十张,才成功~
KEY1键按下:
KEY2键按下:
小结
要实现线程之间的数据传输,非内存池+消息队列莫属了,话说这两个结合在一起是真的好用。任何复杂数据结构都可以封装成结构体,并将其保存在内存池中。需要传输该复杂对象吗?完全不需要!咱不传递该对象本身,我们通过消息队列来传递该对象的地址,有了地址就有了数据本身。传输一个地址,相对传输对象本身,开销还是挺小的。
当然了,本文只是简单介绍了内存池+消息队列的使用,并实现了一个玩具案例。更多的细节啊啥的,需要参考官方资料,以及实际的项目需求,希望对大家有所帮助,任何疑问,欢迎留言,谢谢~
参考资料
以上是关于RTX 线程通信之——内存池的主要内容,如果未能解决你的问题,请参考以下文章