brpc中有两种分配资源的池,分别是object_pool和resource_pool,object_pool是resource_pool的简化版,所以在这里主要介绍resource_pool的原理。
可以对照我画的架构图看更加清楚明了
源码分析
从获取一个对象的方法开始
template <typename T> inline T* get_resource(ResourceId<T>* id) {
return ResourcePool<T>::singleton()->get_resource(id);
}
创建一个T对象,返回一个id
继续
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;
}
__builtin_expect作用类似于linux内核中的likely,用于对分支预测的优化,然后看get_or_new_local_pool的实现
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;
}
template <typename T>
BAIDU_THREAD_LOCAL typename ResourcePool<T>::LocalPool*
ResourcePool<T>::_local_pool = NULL;
resource_pool通过thread_local的pool进行对象的分配,避免了多线程之间的data race和locality。因为使用thread local所以该单例是没有多线程问题的,_change_thread_mutex我看仅用于单元测试中所以不关注。
再继续介绍实现前,先介绍相关的数据结构
ResourcePool* _pool;
Block* _cur_block;
size_t _cur_block_index;
FreeChunk _cur_free;
struct BAIDU_CACHELINE_ALIGNMENT Block {
char items[sizeof(T) * BLOCK_NITEM];
size_t nitem;
Block() : nitem(0) {}
};
typedef ResourcePoolFreeChunk<T, FREE_CHUNK_NITEM> FreeChunk;
static const size_t BLOCK_NITEM = ResourcePoolBlockItemNum<T>::value;
static const size_t FREE_CHUNK_NITEM = BLOCK_NITEM;
template <typename T>
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 ?
ResourcePoolBlockMaxItem<T>::value : N2);
};
template <typename T> struct ResourcePoolBlockMaxSize {
static const size_t value = 64 * 1024; // bytes
};
template <typename T> struct ResourcePoolBlockMaxItem {
static const size_t value = 256;
};
template <typename T, size_t NITEM>
struct ResourcePoolFreeChunk {
size_t nfree;
ResourceId<T> ids[NITEM];
};
有点绕,慢慢梳理:
Block是实际分配内存的数据结构,一块Block有可以分配了存储BLOCK_NITEM个数的空间,Block::nitem表示当前block分配到的对象个数。_cur_block_index代表_cur_block在整个block_groups中的下标索引,block_groups是一个全局的变量,存储多个block_group,每个block_group中又含有多个block。
template <typename T>
butil::static_atomic<typename ResourcePool<T>::BlockGroup*>
ResourcePool<T>::_block_groups[RP_MAX_BLOCK_NGROUP] = {};
struct BlockGroup {
butil::atomic<size_t> nblock;
butil::atomic<Block*> blocks[RP_GROUP_NBLOCK];
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(static_cast<void*>(blocks), 0, sizeof(butil::atomic<Block*>) * RP_GROUP_NBLOCK);
}
};
这就是resource_pool内存分配的基本存储结构,后续都是基于block_group所做的一切辅助索引。
FreeChunk表示被释放的对象,block中分配的对象再使用完回收后不会释放资源,而是会建立freechunk的索引,用于后续分配资源,freechunk存于一个vector中
std::vector<DynamicFreeChunk*> _free_chunks;
资源释放后会push进,在分配时则优先从其中pop出
接下来介绍ResourceId,他也是resouce_pool和object_pool的唯一区别
template <typename T>
struct ResourceId {
uint64_t value;
operator uint64_t() const {
return value;
}
template <typename T2>
ResourceId<T2> cast() const {
ResourceId<T2> id = { value };
return id;
}
};
在resource_pool中,分配的资源都会返回一个ResourceId,而在object_pool中只会返回一个对象指针,实际上这两者并没有太大的区别,区别只在于我们可以通过ResourceId更易于存储,这个id的value其实就是block的索引,通过这个value就能根据索引找到我们的对象,在brpc中bthread以此作为id。
接下来我们继续看LocalPool的get实现,get针对不同参数个数进行重载,这里只介绍一个
#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();
}
有了上述数据结构的了解,这个函数就很好理解了,首先从_cur_free中获取一个回收的对象在block中的index,并以此作为ResourceId,如果_cur_free中已经没有了,则从_free_chunks中pop出来,再进行获取。
如果_free_chunks中也没有可获取的对象,则需要block进行对象的分配,如果当前blcok可分配的对象个数已满,则从全局block_group中添加一个block。
以上就是大致的流程,我们将其中的一些重要环节深入去看实现,首先是pop_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;
}
这个比较浅显易懂,将vector中的freeChunk pop出来,然后将里面的索引memcpy到当前的freechunk中,不再赘述。
接下来是add_block的实现
// 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;
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);
*index = (ngroup - 1) * RP_GROUP_NBLOCK + block_index;
return new_block;
}
g->nblock.fetch_sub(1, butil::memory_order_relaxed);
}
} while (add_block_group(ngroup));
// Fail to add_block_group.
delete new_block;
return NULL;
}
// 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;
}
这里采用了lock-free的实现,new一个block放入BlockGroup中,如果BlockGroup已满,则创建一个BlockGroup放入_block_groups中。
以上就是get_resource的流程,return_resource其实大同小异,回收资源无非就是将资源放入freechunk中,如果已满,则pop到vector中而已。
另外还有根据ResourceId获取对象指针的方法
static inline T* unsafe_address_resource(ResourceId<T> id) {
const size_t block_index = id.value / BLOCK_NITEM;
return (T*)(_block_groups[(block_index >> RP_GROUP_NBLOCK_NBIT)]
.load(butil::memory_order_consume)
->blocks[(block_index & (RP_GROUP_NBLOCK - 1))]
.load(butil::memory_order_consume)->items) +
id.value - block_index * BLOCK_NITEM;
}
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;
}
ResourceId作为索引将其切分作为不同数组的下标获取到对应block下的对象位置。
注意
- 一个block只会被一个线程所使用,而block的分配是从全局的block_group中所分配,所以free_chunk所存的id一定是自己线程所释放的对象,但当free_chunk达到一定数量时,push进的free_chunks则是针对整个对象的,即全局的对象,所以pop的时候需要加锁。
- 被回收的对象并没有被真正释放,所以通过ResourceId是可以依然访问被释放的对象的,所以存在ABA的问题,需要用户在上层通过version技术去规避。
总结
brpc的资源池实现还是比较容易理解的,与我曾经看过的资源池大同小异。我自己印象比较深刻的点,一是brpc非常看重locality,通过thread local来实现高效的资源分配,并通过一个全局的blockgroup来进行分发blcok,再通过一个全局的freechunks来进行回收object,实现资源的合理调度。同时为了防止多线程下分发blcok导致的锁竞争导致的性能下降,采取了lock-free的实现方式。所以虽然能看明白大概的原理,但如果要抠lock-free的实现细节,其实还是比较复杂的,每一个原子变量的参数该怎么设置,如何避免各种情况下的多线程竞争,这需要作者具有很强的并发编程能力,这也是我所需要学习的。但这片文章的目的是弄明白资源池的原理,所以就先止步于此吧!