2021SC@SDUSC
一、resource pool类简介
上篇文章简单介绍了bthread的概念及作用,并且分析了同步的工具——butex,这篇文章继续介绍一个bthread的基础类,resource pool。
resource pool是非常基础且重要的底层工具类之一,中文翻译过来就是资源池,是一个用于在多线程环境下进行资源分配和回收的类,也可以理解将它的功能为高度竞争环境下速度更快的new和delete。resource pool在brpc里面使用很广泛,在这里介绍主要是因为它用在了bthread的taskmeta分配(因为taskmeta对于创建的速度有着很高的要求),所以去看了这个基础类,我们可以从这里看到一些brpc对性能的追求,而且因为需要支持各种类型,这个类也很好地展现了c++模板的使用。
二、代码分析
resource pool的主要作用就是资源分配,可以看到最常用的函数当然是分配和回收资源,直接供外部调用的函数如下。
get_resource和new类似,是获取对象的,这边提供了三个方法重载,分别对应没有参数、1个参数和2个参数构造对应类型对象的情况,内部则都是调用ResourcePool::singleton()->get_resource。
调用的是ResourcePool::singleton()是获取对应类型资源池的单例,然后再从中分配回收资源。
_singleton的定义如下,这是一个ResourcePool<T>类型的静态原子变量,也就是对每一种不同的数据类型T都有与之对应的变量,也就是说对于每一种数据类型在调用resource pool后会有自己的资源池单例。而这会儿看上面的singleton()函数就是一个很简单的操作:如果已经初始化了直接返回单例,没初始化则新建并返回单例的函数。同时因为是多线程调用所以用pthread_mutex_lock加了锁,并且使用了memory_order_release/consume来保证某线程的新建对其他线程读取的可见性。
然后我们来看看获取到单例后,调用的ResourcePool单例内部的 get_resource(),有如下几个重载。
这三个get_resource()都是先调用get_or_new_local_pool()获取到一个LocalPool类型的指针lp,随后使用指针调用get,传入id和附加的参数,get_or_new_local_pool()函数如下。首先判断_local_pool是否已经有了,如果有则直接返回,如果没有则新建后返回,新建成功后会调用thread_atexit登记一个pthread退出后删除_local_pool的函数,同时给_nlocal加一,该变量表明T类型的resource pool的local pool数量。
_local_pool是一个thread local变量,每个pthread都会有一个,注意LocalPool这个类型,它是整个resource pool的资源分配的入口,构造的时候会传入全局的单例resource pool,_local_pool定义声明如下。
调用的三个get如下。
上面的宏定义实现如下,先解释一下注释中提到的POD。
POD全称Plain Old Data是指C风格的struct结构体定义的数据结构,其中struct结构体中只能定义常规数据类型(不能含有自定义数据类型)。它仅作为被动的收藏的字段值,不使用封包或者other object-oriented特征。
对于POD类型T的对象,不管这个对象是否拥有类型T的有效值,如果将该对象的底层字节序列复制到一个字符数组(或者无符号字符数组)中,再将其复制回对象,那么该对象的值与原始值一样。
对于任意的POD类型T,如果两个T指针分别指向两个不同的对象obj1和obj2,如果用memcpy库函数把obj1的值复制到obj2,那么obj2将拥有与obj1相同的值。
简言之,针对POD对象,其二进制内容是可以随便复制的,在任何地方,只要其二进制内容在,就能还原出正确无误的POD对象。对于任何POD对象,都可以使用memset()函数或者其他类似的内存初始化函数。
这里是想要调用new T而不是 new T()来避免不必要的memset,节省开销。
首先我们先介绍一些其他的定义再看GET的实现。
ResourceId结构体包含一个uint64_t 的value,通过重载uint64_t运算符可以直接当uint64_t类型使用,还有一个模板函数可以实现不同类型的ResourceId转换,ResourceId是resource pool中某个资源的唯一标识identifier,所有的资源获取和归还都是基于ResourceId的,比如获取资源就是resource pool返回资源并将资源id写入到传入的ResourceId里。
resource pool在内存分配上是按块进行,Block是ResourcePool类里的一个struct,如下。nitem是当前block里已建立的item的数量,items数组本质上则是提前分配好的内存。
Block则是受BlockGroup管理,nblock表示块的数量,blocks[]存储对应指针。
ResourcePoolFreeChunk是空闲的Chunk(指用户请求分配的内存),里面有两个变量,空闲的资源个数,和id数组。
关于它还有两个typedef。FreeChunk是固定大小为FREE_CHUNK_NITEM的一个chunk,而DynamicFreeChunk是利用动态数组实现的一个变长的chunk。
然后我们就可以回去看GET怎么被实现的了。
首先当本地已有空闲的资源和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取出返回。
如果本地的chunk没有空闲资源,那么去看有没有全局的free_chunk,_pool->pop_free_chunk(_cur_free)是从全局取一个free_chunk赋值给_cur_free,如果取到了,则进行和上面一样的操作。
pop_free_chunk函数如下。就是从_free_chunks尾部取一个freechunk拷贝给传进来的freechunk,注意这里拷贝的是空闲的resourceId,根据resourceId可以计算出资源所在块。
如果进行到这里,说明全局也没有已有的空闲的资源了,这个时候优先考虑从本地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类型的对象。
如果依旧不行,则先新建一个block把指针赋给_cur_block,在block里新建对象。add_block函数不再多说。
接下来再看回收资源,在外部调用的是return_resource函数。对应使用的内部函数在这里。其实是上面分配的反过程。优先返回到本地的_cur_free,如果_cur_free满了,则把_cur_free push到全局的_free_chunks里,然后把当前归还的id放到_cur_free里。
push基本和上面的pop相反过来。