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语义来保证某线程的新建对其他线程读取的可见性。
获取到单例后,调用的是内部的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
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函数如下:
归还资源在外部是调用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的动态内存分配可以节省空间。
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数据的拷贝,消耗还是可接受的。总的来说,就是利用提前的块内存分配以及对象的重复利用,实现了高性能的资源分配。