ResourceId
定义
struct ResourceId {
uint64_t value;
operator uint64_t() const {
return value;
}
template <typename T2>
ResourceId<T2> cast() const {
ResourceId<T2> id = { value };
return id;
}
};
实际上就是一个uint64_t。
ResourceId是resource pool中某个资源的唯一标识,所有的资源获取和归还都是基于ResourceId的
ResourcePoolFreeChunk
定义
template <typename T, size_t NITEM>
struct ResourcePoolFreeChunk {
size_t nfree;
ResourceId<T> ids[NITEM];
};
// for gcc 3.4.5
template <typename T>
struct ResourcePoolFreeChunk<T, 0> {
size_t nfree;
ResourceId<T> ids[0];
};
// class ResourcePool中声明
typedef ResourcePoolFreeChunk<T, FREE_CHUNK_NITEM> FreeChunk;
typedef ResourcePoolFreeChunk<T, 0> DynamicFreeChunk;
模板传入两个参数,类型T和长度NITEM。当NITEM=0时,成员变量ids为柔性数组。
成员变量中,ids存储若干个ResourceId,元素个数为nfree。
顾名思义,FreeChunk即存储空闲的Chunk(块),ids中的即为空闲块。chunk实际上就是被分配出去又归还回来的resouceid集合。
ResourcePool
ResourcePool其实可以分为两个模块,一个是实际存放资源的内存模块(Block, BlockGroup),一个是存放归还回来空间对应ResourceId的数据结构(ResourcePoolFreeChunk)。ResourcePool使用thread local的LocalPool来管理当前线程的资源分配,尽量减少数据竞争。但LocalPool内资源用尽/初始无资源还是由ResourcePool进行分配。分配资源的基本原则是,先从回收的空闲资源分配,再从内存模块中分配。
成员变量
一些重要的成员变量
static BAIDU_THREAD_LOCAL LocalPool* _local_pool; // 每个线程有一个
static butil::static_atomic<long> _nlocal; // local pool数量
static butil::static_atomic<size_t> _ngroup; // 当前使用的block_group数量
static butil::static_atomic<BlockGroup*> _block_groups[RP_MAX_BLOCK_NGROUP]; // RP_MAX_BLOCK_NGROUP=65536,上限内存65536*65536*65536B,上限资源个数65536*65536*256。
std::vector<DynamicFreeChunk*> _free_chunks; // 用来缓存已经分配出去又回收的ResourceId。相当于是一个二维的resourceId*数组,DynamicFreeChunk是变长的。
_block_groups初始仅创建BlockGroup类型数组,并没有真正申请内存。当需要用到的时候Block的时候,再去申请BlockGroup空间,此时也是仅分配了Block数组的空间;再申请Block空间,才是真正分配了资源空间。 加上资源可以回收再分配,很难全用完。
但这里有个问题,当某个资源用完了返回给resourcePool的时候,是放在_free_chunks中的,并不会delete让系统回收,所以如果需要短时间分配大量资源,用完之后进程占用的系统内存是不会降回来的。该问题在github上也有反馈
注意_free_chunks使用需要注意线程间竞争问题,但_free_chunks中的单个DynamicFreeChunk仅会被单个线程使用。
以上变量主要属于以下内部类
内部类
Block
template <typename T> struct ResourcePoolBlockMaxSize {
static const size_t value = 64 * 1024; // bytes
};
template <typename T> struct ResourcePoolBlockMaxItem {
static const size_t value = 256;
};
class ResourcePoolBlockItemNum {
static const size_t N1 = ResourcePoolBlockMaxSize<T>::value / sizeof(T);
static const size_t N2 = (N1 < 1 ? 1 : N1);
public:
static const size_t value = (N2 > ResourcePoolBlockMaxItem<T>::value ? // 最大256
ResourcePoolBlockMaxItem<T>::value : N2);
};
static const size_t BLOCK_NITEM = ResourcePoolBlockItemNum<T>::value; // 如果sizeof(T)>256,BLOCK_NITEM <256,BLOCK_NITEM*sizeof(T)=65536
struct BAIDU_CACHELINE_ALIGNMENT Block {
char items[sizeof(T) * BLOCK_NITEM];
size_t nitem;
Block() : nitem(0) {}
};
// 生成Block,ResourcePool成员函数
// Create a Block and append it to right-most BlockGroup.
static Block* add_block(size_t* index) {
Block* const new_block = new(std::nothrow) Block; // 申请Block资源
if (NULL == new_block) {
return NULL;
}
size_t ngroup;
do {
ngroup = _ngroup.load(butil::memory_order_acquire);
if (ngroup >= 1) {
BlockGroup* const g =
_block_groups[ngroup - 1].load(butil::memory_order_consume);
const size_t block_index =
g->nblock.fetch_add(1, butil::memory_order_relaxed);
if (block_index < RP_GROUP_NBLOCK) {
g->blocks[block_index].store(
new_block, butil::memory_order_release); // 将新生成的block存入blockGroup中
*index = (ngroup - 1) * RP_GROUP_NBLOCK + block_index; // 返回下标逻辑是将二维下标转成一维
return new_block; // 返回给localpool使用
}
g->nblock.fetch_sub(1, butil::memory_order_relaxed);
}
} while (add_block_group(ngroup)); // 当初始_block_groups数组中只有空指针时,申请一个BlockGroup资源
// Fail to add_block_group.
delete new_block;
return NULL;
}
这里的Block是ResourcePool的内部类,和上期讲的Block虽然不是一个东西,但是用途类似。实际上是内存块,大小一般是65536B,ResourcePool的数据都是放在Block中的。Block中有一个items用于存储实际数据,nitem表示中的bytes数。
一般情况下,brpc中使用到ResourcePool的类大小都会>256B,所以一般情况下一个Block大小是65536B。
Block是非线程安全的,只能在单个线程中操作。
BlockGroup
static const size_t RP_GROUP_NBLOCK_NBIT = 16;
static const size_t RP_GROUP_NBLOCK = (1UL << RP_GROUP_NBLOCK_NBIT); // 65536
struct BlockGroup {
butil::atomic<size_t> nblock;
butil::atomic<Block*> blocks[RP_GROUP_NBLOCK]; // 共65536个Block,一共65536*65536B=4G
BlockGroup() : nblock(0) {
// We fetch_add nblock in add_block() before setting the entry,
// thus address_resource() may sees the unset entry. Initialize
// all entries to NULL makes such address_resource() return NULL.
memset(blocks, 0, sizeof(butil::atomic<Block*>) * RP_GROUP_NBLOCK);
}
};
// 生成BlockGroup,ResourcePool成员函数
// Create a BlockGroup and append it to _block_groups.
// Shall be called infrequently because a BlockGroup is pretty big.
static bool add_block_group(size_t old_ngroup) {
BlockGroup* bg = NULL;
BAIDU_SCOPED_LOCK(_block_group_mutex);
const size_t ngroup = _ngroup.load(butil::memory_order_acquire);
if (ngroup != old_ngroup) {
// Other thread got lock and added group before this thread.
return true;
}
if (ngroup < RP_MAX_BLOCK_NGROUP) {
bg = new(std::nothrow) BlockGroup;
if (NULL != bg) {
// Release fence is paired with consume fence in address() and
// add_block() to avoid un-constructed bg to be seen by other
// threads.
_block_groups[ngroup].store(bg, butil::memory_order_release);
_ngroup.store(ngroup + 1, butil::memory_order_release);
}
}
return bg != NULL;
}
ResourcePool内部类,内有一个blocks数组,以及nblock标识block数量。一个Block数组,用于管理Block(实际上BlockGroup数组相当于生成一个二维的Block*数组?)。
BlockGroup是线程安全的,一个BlockGroup中的各个Block,可能给不同线程使用.
LocalPool
ResourcePool内部类。在一个进程下,每个模板T中的所有ResourcePool对象(实际只有一个),在每个线程中,共用一个LocalPool。每个线程只会从自己的LocalPool中获取数据,减少数据竞争。
// Each thread has an instance of this class.
class BAIDU_CACHELINE_ALIGNMENT LocalPool {
private:
ResourcePool* _pool; // LocalPool所属的ResourcePool,在ResourcePool中生成LocalPool时会把this传进来
Block* _cur_block; // 如果_cur_free没有可用资源,则从_cur_block中按顺序分配
size_t _cur_block_index; // _cur_block在所属的ResourcePool的_block_groups中的下标。_cur_block = _block_groups[i][j], _cur_block_index=i*RP_GROUP_NBLOCK+j
FreeChunk _cur_free; // 缓存空闲的resourceId资源,主要是一些已经分配出去了的又返还回来的零碎resourceId资源。在分配资源时优先使用这些资源。
};
static BAIDU_THREAD_LOCAL LocalPool* _local_pool; // ResourcePool中对LocalPool的声明,静态变量+thread local
// 生成localPool,ResourcePool成员函数
inline LocalPool* get_or_new_local_pool() {
LocalPool* lp = _local_pool;
if (lp != NULL) {
return lp;
}
lp = new(std::nothrow) LocalPool(this);
if (NULL == lp) {
return NULL;
}
BAIDU_SCOPED_LOCK(_change_thread_mutex); //avoid race with clear()
_local_pool = lp;
butil::thread_atexit(LocalPool::delete_local_pool, lp);
_nlocal.fetch_add(1, butil::memory_order_relaxed);
return lp;
}
LocalPool管理当前线程的资源分配,获取资源的get_resource函数,实际也是调用LocalPool的get函数。
LocalPool成员函数:
-
get
// We need following macro to construct T with different CTOR_ARGS // which may include parenthesis because when T is POD, "new T()" // and "new T" are different: former one sets all fields to 0 which // we don't want. #define BAIDU_RESOURCE_POOL_GET(CTOR_ARGS) \ /* Fetch local free id */ \ if (_cur_free.nfree) { \ const ResourceId<T> free_id = _cur_free.ids[--_cur_free.nfree]; \ *id = free_id; \ BAIDU_RESOURCE_POOL_FREE_ITEM_NUM_SUB1; \ return unsafe_address_resource(free_id); \ } \ /* Fetch a FreeChunk from global. \ TODO: Popping from _free needs to copy a FreeChunk which is \ costly, but hardly impacts amortized performance. */ \ if (_pool->pop_free_chunk(_cur_free)) { \ --_cur_free.nfree; \ const ResourceId<T> free_id = _cur_free.ids[_cur_free.nfree]; \ *id = free_id; \ BAIDU_RESOURCE_POOL_FREE_ITEM_NUM_SUB1; \ return unsafe_address_resource(free_id); \ } \ /* Fetch memory from local block */ \ if (_cur_block && _cur_block->nitem < BLOCK_NITEM) { \ id->value = _cur_block_index * BLOCK_NITEM + _cur_block->nitem; \ T* p = new ((T*)_cur_block->items + _cur_block->nitem) T CTOR_ARGS; \ if (!ResourcePoolValidator<T>::validate(p)) { \ p->~T(); \ return NULL; \ } \ ++_cur_block->nitem; \ return p; \ } \ /* Fetch a Block from global */ \ _cur_block = add_block(&_cur_block_index); \ if (_cur_block != NULL) { \ id->value = _cur_block_index * BLOCK_NITEM + _cur_block->nitem; \ T* p = new ((T*)_cur_block->items + _cur_block->nitem) T CTOR_ARGS; \ if (!ResourcePoolValidator<T>::validate(p)) { \ p->~T(); \ return NULL; \ } \ ++_cur_block->nitem; \ return p; \ } \ return NULL; \ inline T* get(ResourceId<T>* id) { BAIDU_RESOURCE_POOL_GET(); } template <typename A1> inline T* get(ResourceId<T>* id, const A1& a1) { BAIDU_RESOURCE_POOL_GET((a1)); } template <typename A1, typename A2> inline T* get(ResourceId<T>* id, const A1& a1, const A2& a2) { BAIDU_RESOURCE_POOL_GET((a1, a2)); }
获取资源、分配resourceId的函数。宏BAIDU_RESOURCE_POOL_GET的注释提到,
new T()
和new T
是不一样的,前者会对所有字段初始化为0,后者不会,brpc中大部分使用的都是后者。
函数处理逻辑如下:
1. 调用get时,首先判断_cur_free中是否还有空闲的Chunk,有即从_cur_free数组末端取出空闲位置的ResourceId,并将该ResourceId从_cur_free.ids去掉。获取到空闲位置的ResourceId后,调用unsafe_address_resource从内存块中找到ResourceId对应的地址,则这块大小为T的内存就分配出来供业务使用了。unsafe_address_resource逻辑后面单独写。
2. 如果没有从_cur_free中获取到空闲的resouceid,则再尝试从ResourcePool中的_free_chunks中pop出一个FreeChunk来,作为当前的_cur_free,再从_cur_free中取,和1一样。
3. 如果当前没有空闲的chunk了,但当前_cur_block不为空,并且_cur_block的元素没有超上限(65536/sizeof(T)),则从cur_block中末尾向后占用sizeof(T)大小的空间,分配给当前申请的资源,更新_cur_block的占用情况。找到存放资源的内存位置之后,就可以根据它在block中的位置生成ResourceId,生成资源并返回。生成ResourceId的逻辑在介绍ResourceId的位置细讲。
4. _cur_block为空或_cur_block里的元素超过上限了,则需要向ResourcePool申请一个新的block作为_cur_block,申请逻辑放在add_block()函数中讲。有了可用的_cur_block后就可以按照步骤3分配资源并返回。
5. 前面说到,假如_cur_block = _block_groups[i][j], 则_cur_block_index=iRP_GROUP_NBLOCK+j,那么resourceId=_cur_block_indexBLOCK_NITEM +_cur_block->nitem,即resourceId的生成规则 -
return_resource
inline int return_resource(ResourceId<T> id) { // Return to local free list if (_cur_free.nfree < ResourcePool::free_chunk_nitem()) { _cur_free.ids[_cur_free.nfree++] = id; BAIDU_RESOURCE_POOL_FREE_ITEM_NUM_ADD1; return 0; } // Local free list is full, return it to global. // For copying issue, check comment in upper get() if (_pool->push_free_chunk(_cur_free)) { _cur_free.nfree = 1; _cur_free.ids[0] = id; BAIDU_RESOURCE_POOL_FREE_ITEM_NUM_ADD1; return 0; } return -1; }
如果_cur_free未满,就归还到_cur_free中。_cur_free满了则先将整个_cur_free归还回ResourcePool中的_free_chunks,再将id归还到 _cur_free中。
成员函数及ResourcePool相关函数
-
get_resource
inline T* get_resource(ResourceId<T>* id) { LocalPool* lp = get_or_new_local_pool(); if (__builtin_expect(lp != NULL, 1)) { return lp->get(id); } return NULL; } template <typename A1> inline T* get_resource(ResourceId<T>* id, const A1& arg1) { LocalPool* lp = get_or_new_local_pool(); if (__builtin_expect(lp != NULL, 1)) { return lp->get(id, arg1); } return NULL; } template <typename A1, typename A2> inline T* get_resource(ResourceId<T>* id, const A1& arg1, const A2& arg2) { LocalPool* lp = get_or_new_local_pool(); if (__builtin_expect(lp != NULL, 1)) { return lp->get(id, arg1, arg2); } return NULL; }
-
return_resource
inline int return_resource(ResourceId<T> id) { LocalPool* lp = get_or_new_local_pool(); if (__builtin_expect(lp != NULL, 1)) { return lp->return_resource(id); } return -1; }
都很眼熟,前面LocalPool部分阐述过具体逻辑。
-
address_resource
static inline T* address_resource(ResourceId<T> id) { const size_t block_index = id.value / BLOCK_NITEM; const size_t group_index = (block_index >> RP_GROUP_NBLOCK_NBIT); if (__builtin_expect(group_index < RP_MAX_BLOCK_NGROUP, 1)) { BlockGroup* bg = _block_groups[group_index].load(butil::memory_order_consume); if (__builtin_expect(bg != NULL, 1)) { Block* b = bg->blocks[block_index & (RP_GROUP_NBLOCK - 1)] .load(butil::memory_order_consume); if (__builtin_expect(b != NULL, 1)) { const size_t offset = id.value - block_index * BLOCK_NITEM; if (__builtin_expect(offset < b->nitem, 1)) { return (T*)b->items + offset; } } } } return NULL; }
前面说过id的生成规则,那么反向操作即可找到对应的Block以及地址偏移量,即可取到对应的地址。
-
pop_free_chunk/push_free_chunk
bool pop_free_chunk(FreeChunk& c) { // Critical for the case that most return_object are called in // different threads of get_object. if (_free_chunks.empty()) { return false; } pthread_mutex_lock(&_free_chunks_mutex); if (_free_chunks.empty()) { pthread_mutex_unlock(&_free_chunks_mutex); return false; } DynamicFreeChunk* p = _free_chunks.back(); _free_chunks.pop_back(); pthread_mutex_unlock(&_free_chunks_mutex); c.nfree = p->nfree; memcpy(c.ids, p->ids, sizeof(*p->ids) * p->nfree); free(p); return true; } bool push_free_chunk(const FreeChunk& c) { DynamicFreeChunk* p = (DynamicFreeChunk*)malloc( // 申请一个DynamicFreeChunk 大小与c一致 offsetof(DynamicFreeChunk, ids) + sizeof(*c.ids) * c.nfree); // offsetof:ids在DynamicFreeChunk中的偏移量 if (!p) { return false; } p->nfree = c.nfree; memcpy(p->ids, c.ids, sizeof(*c.ids) * c.nfree); pthread_mutex_lock(&_free_chunks_mutex); _free_chunks.push_back(p); pthread_mutex_unlock(&_free_chunks_mutex); return true; }
pop_free_chunk拿到_free_chunks中最后一个DynamicFreeChunk,复制到FreeChunk中即可使用。
push_free_chunk则申请一个DynamicFreeChunk,大小与需要存储的FreeChunk c一致,并将c拷贝到DynamicFreeChunk中,加到_free_chunks中。
注意_free_chunks的操作都要加锁。
了解了以上内部类之后,我们对ResourcePool可以有个基本的认识。ResourcePool由若干个BlockGroup组成,BlockGroup由若干个Block组成,在一个Block上,最多可以分配256个对象。用户可以通过ResourceId来从具体的Block上找到对应对象的地址。为了做到资源的回收利用,ResourcePool还包括一个用来存储ResourceId列表的数据结构,即DynamicFreeChunk(FreeChunk)列表。因为ResourcePool是一个单例,所有进程共用一个ResourcePool。为了减少进程间的竞争,还引入了LocalPool。每个LocalPool包含一个Block和一个FreeChunk,FreeChunk可以回收并重新利用资源,Block可以直接下发资源。当LocalPool的资源用尽,将重新去所属ResourcePool中申请FreeChunk或Block。
ObjectPool
ObjectPool实际上是ResourcePool的变种,也有Group、TaskGroup、FreeChunk等结构,唯一不同的是生成的对象不会返回一个ResourceId,仅返回对象指针。看具体需要决定使用哪种类型。比如后面说到的TaskMeta使用的是resourcePool,TaskMeta中的栈空间使用的是ObjectPool