brpc源码解析(五)—— 基础类resource pool详解


brpc是一个完整的rpc框架,其中用到了很多比较优秀的基础类,会看情况分析下这些类的源码,从这篇的resource pool开始,resource pool是很重要的基础类之一,顾名思义,就是资源池,是一个用于在多线程环境下进行资源分配和回收的类,可以理解为高度竞争环境下速度更快的new和delete。resource pool在brpc里面有着大量的使用,比较典型的,socket的分配和bthread 的taskmeta分配都是用的resource pool,因为二者对于创建的速度有着很高的要求。而且因为需要支持各种类型,这个类也很好地诠释了c++模板的使用。

既然功能是资源分配,最常用的自然是分配和回收资源,直接供外部调用的函数如下:
在这里插入图片描述
get_resource和new类似,是获取对象的,这边提供了三个重载,分别对应没有参数、1个参数和2个参数构造对应类型对象的情况,而内部则是调用ResourcePool::singleton()->get_resource,注意这几个函数并不是类函数,不要和后面resource pool内部的get_resource弄混了,这里贴一个以前介绍过的socket create里实际的调用:
在这里插入图片描述
回到上面的ResourcePool::singleton()->get_resource,ResourcePool::singleton()是获取对应类型资源池的单例,具体的函数如下:
在这里插入图片描述
其中_singleton变量定义声明如下:
在这里插入图片描述
在这里插入图片描述
这是一个ResourcePool*类型的静态原子变量,也就是对每一种不同的T都有与之对应的变量,换句话说,就是每一种类型在调用resource pool后会有自己的资源池单例。而整个singleton()函数则是很常见的一个如果已经初始化了直接返回单例,没初始化则新建并返回单例的函数,因为是多线程调用所以用pthread_mutex_lock加了锁,并使用release-consume语义来保证某线程的新建对其他线程读取的可见性。

1.获取资源

获取到单例后,调用的是内部的ResourcePool单例内部的 get_resource,有如下几个重载:
在这里插入图片描述
这几个重载都是先调用get_or_new_local_pool()获取到一个LocalPool类型的指针lp,随后使用指针调用get,get_or_new_local_pool()函数如下:
在这里插入图片描述
_local_pool是一个thread local变量,也就是每个pthread会有一个,注意LocalPool这个类型,它是整个resource pool的资源分配的入口,构造的时候会传入全局的单例resource pool,_local_pool定义声明如下:
在这里插入图片描述
在这里插入图片描述
get_or_new_local_pool()里,首先判断_local_pool是否已经有了,如果有则直接返回,如果没有则新建后返回,新建成功后会调用thread_atexit登记一个pthread退出后删除_local_pool的函数,同时给_nlocal加一,该变量表明T类型的resource pool的local pool数量。
拿到local pool的指针lp后就会调用lp->get,local pool的三个get函数如下:
在这里插入图片描述
这三个重载都是一个BAIDU_RESOURCE_POOL_GET的宏定义,之所以要用宏定义来实现对不同个数参数的重载,注释里给了解释:
在这里插入图片描述
POD指的是Plain Old Data,基本数据类型、指针、union、数组、trivial构造函数的 struct或者 class都属于这类数据,这类数据属于C++中与C相兼容的数据类型,可以按照C的方式来处理(运算、拷贝等)。这里是想要调用new T而不是 new T()来避免不必要的memset,节省开销。
在介绍取Resource的机制前,先介绍下ResourceId,ResourceId是一个模板struct,如下:
在这里插入图片描述
包含一个uint64_t 的value,通过重载uint64_t运算符可以直接当uint64_t类型使用,还有一个模板函数可以实现不同类型的ResourceId转换,ResourceId是resource pool中某个资源的唯一标识,所有的资源获取和归还都是基于ResourceId的,比如获取资源就是resource pool返回资源并将资源id写入到传入的ResourceId里,在上面那个socket的例子里,就是slot变量,该变量会用于组成socket的版本。
resource pool在内存分配上是按块来的,Block是ResourcePool类里的一个struct,如下:
在这里插入图片描述
NItem是当前block里已建立的item的数量,初始为0,而items数组实质上则是提前分配好的内存。
Block则是受BlockGroup管理,BlockGroup初始化block数量为0,把blocks数组里的所有指针都初始化为NULL。
在这里插入图片描述
ResourcePoolFreeChunk是一个模板struct,顾名思义,是空闲的Chunk,里面有两个变量,空闲的资源个数,和id数组:
在这里插入图片描述
ResourcePool与之相关的两个typedef如下:
在这里插入图片描述
这两个的区别就在于,FreeChunk是固定大小为FREE_CHUNK_NITEM的一个chunk,而DynamicFreeChunk是利用柔性数组实现的一个变长的chunk,使用柔性数组可以节省内存,后面会详细展开解释。
在BAIDU_RESOURCE_POOL_GET宏定义里,依次按照如下几个步骤尝试去拿需要的资源,

1.本地已有空闲的资源和id,直接从block中找到资源返回
在这里插入图片描述
_cur_free是一个FreeChunk变量,也就是当前thread的local pool里的空闲资源chunk,nfree是该chunk空闲资源个数,如果大于0,则根据nfree在对应位置取出一个free_id,赋值给传进来的参数id,并且调用unsafe_address_resource去取出资源,unsafe_address_resource就是根据id算出位置,去相应的block group和里面的block取,如下:
在这里插入图片描述
除了unsafe_address_resource还有一个类似的address_resource函数,增加了一些合法性的判断,性能相对差点,用于给外部调用:
在这里插入图片描述

2.全局有空闲的资源和id
在这里插入图片描述
如果本地的chunk没有空闲资源,则看有没有全局的free chunk,_pool->pop_free_chunk(_cur_free)是从全局取一个free_chunk赋值给_cur_free,如果取到了,则进行和步骤1一样的操作。pop_free_chunk函数如下:
在这里插入图片描述
_free_chunks是resource pool的一个std::vector<DynamicFreeChunk*>类型的类变量,DynamicFreeChunk前面说过了,是一个可变长度的freechunk,整个函数就是从_free_chunks尾部取一个freechunk拷贝给传进来的freechunk,注意这里拷贝的是空闲的resourceId,根据resourceId可以计算出资源所在块。

3.在本地block新建资源实例
在这里插入图片描述
如果_free_chunks为空,说明全局也没有已有的空闲的资源了,这个时候优先考虑从本地block上新建对象, _cur_block 是local pool里的Block类型的类变量。如果_cur_block里已有的item数量小于上限,则直接在里面新建一个对象,id->value指明了新建对象在clock里的位置,address会用到。注意T p = new ((T*)_cur_block->items + _cur_block->nitem) T CTOR_ARGS;这个语句,是在不分配内存的情况下指定位置直接新建对象,也就是在((T*)_cur_block->items + _cur_block->nitem)指明的内存空间上新建一个T类型的对象。

4.如果_cur_block没初始化或者已经满了,则先新建一个block把指针赋给_cur_block,在block里新建对象。
在这里插入图片描述
add_block函数如下:
在这里插入图片描述

2.归还资源

归还资源在外部是调用return_resource函数:
在这里插入图片描述
对应的resourcepool里的return_resource函数如下:
在这里插入图片描述
优先归还到本地的_cur_free,如果_cur_free满了,则把_cur_free push到全局的_free_chunks里,然后把当前归还的id放到_cur_free里。
push_free_chunk函数如下:
在这里插入图片描述
DynamicFreeChunk* p的内存分配就利用了柔性数组的特性,根据c的大小来进行内存分配,体现了Dynamic。push_free_chunk在当前线程退出local pool被析构的时候也要被调用:
在这里插入图片描述
这种情况下_cur_free.nfree是不确定的, DynamicFreeChunk的动态内存分配可以节省空间。

3.总结

resource pool使用block和block group来管理内存,对象都是在block里分配的,新建对象会根据块容量等算出一个resourceId,后续的获取和归还都用根据这个Id算出便宜量来定位到具体的资源位置。

resource pool全局只有一个单例,blockgroup和block都是resource pool单例层面的,而每个pthread会有一个thread local 的local pool,local pool有一个FreeChunk类型的_cur_free变量,保存着局部的空闲资源的id,resource pool里还有一个全局的_free_chunks变量,保存着全局空闲资源的id,Localpool还有一个Block*类型的_cur_block变量,一个指向resource pool某个block的指针。

对于获取资源,如果_cur_free还有空闲的对象资源Id,那么直接找到对应资源返回,否则看_free_chunks里有没有FreeChunk,有的话拷贝到_cur_free,然后取出空闲的对象资源Id,找到对应资源返回。如果局部和全局都没有已有空闲资源则优先在_cur_block上新创建,如果这个指针为null或者指向的block已经满了,则调用resource pool的add_block新建一个block并赋值给_cur_block,并在上创建对象。这里提到的创建对象都是在已有内存上创建,没有分配内存的过程,所以比较快。

对于归还资源,则是优先将id存在_cur_free,如果_cur_free满了则把_cur_free push到_free_chunks里,空_cur_free后再将当前id放入_cur_free里。

无论是获取还是归还都会有在局部空闲id列表和全局空闲id列表之间进行memcpy的情况,是比较耗性能的,但是因为仅仅是很小的resourceId数据的拷贝,消耗还是可接受的。总的来说,就是利用提前的块内存分配以及对象的重复利用,实现了高性能的资源分配。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值