前面跟bthread流程的时候多个地方涉及资源池都没有细看,本来这部分应该放到bthread前面的,但因为对框架不熟悉,只能从提供的demo开始。
ResourcePool是一个全局的单例模式类,而每个线程(thread_local)拥有一个单例的LocalPool,大概思想是线程通过LocalPool请求对象,LocalPool每一次从ResourcePool中请求一个大的块(就叫做Block吧),ResourcePool中每次内存不足时向系统请求一个Block。
Block的管理
ResourcePool是一个模版类,意味着对每个类型T都有一个池子。首先我们想一想Block该如何保存?如果采用一个定长的数组,速度很快,但是很容易超出上限;如果采用vector来管理,当每次vector容量加倍时需要进行一次整体的拷贝,开销会较大。
bthread采用了二层索引式管理:第一是采用BlockGroup用来存储一系列Block指针,注意这里64位对齐是为了保证整个Block在一个Cache Line中,这样访问速度会快一些。
template<class T>
struct __attribute__((alignment(64))) Block {
char items[sizeof(T) * BLOCK_NITEM];
size_t nitem;
Block() : nitem(0) {}
};
template<class T>
struct BlockGroup {
std::atomic<size_t> nblock;
std::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);
}
};
第二层采用BlockGroup数组来存储BlockGroup指针
class ResourcePool {
private:
static std::atomic<size_t> _ngroup;
static pthread_mutex_t _block_group_mutex;
static std::atomic<BlockGroup*> _block_groups[RP_MAX_BLOCK_NGROUP];
};
多层索引式的缺点是空间局部性不好,然而分配了之前的Block之后,除非要释放它,基本不会去访问,所以不存在这个问题。
对象的id
bthread采取了给每个对象赋一个id的方法,这样就能在任何时候轻松找到这个对象所在的Block。
这里为什么不直接使用uint64_t来表示id呢?因为我们在返还对象时,直接使用ResourceId作为参数,编译器能够判断这个对象所属的类型。
unsafe_address_resource()是根据id计算出这个对象的地址。
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;
}
};
template<class T>
class ResourcePool {
private:
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;
}
};
返还对象的管理
在正式谈论请求和返还对象操作之前,我们有必要预想如何管理返还的对象?
bthread采取了最简单的方式:直接把其id加入一个空闲列表。
template <typename T, size_t NITEM>
struct ResourcePoolFreeChunk {
size_t nfree;
ResourceId<T> ids[NITEM];
};
template <typename T>
struct ResourcePoolFreeChunk<T, 0> {
size_t nfree;
ResourceId<T> ids[0];
};
template<class T>
class ResourcePool {
private:
typedef ResourcePoolFreeChunk<T, 0> DynamicFreeChunk;
std::vector<DynamicFreeChunk*> _free_chunks;
pthread_mutex_t _free_chunks_mutex;
};
template<class T>
class LocalPool {
private:
typedef ResourcePoolFreeChunk<T, SIZE> FreeChunk;
ResourcePool* _pool;
Block* _cur_block;
size_t _cur_block_index;
FreeChunk _cur_free;
};
在LocalPool中的使用的是长度统一的空闲列表,而在ResourcePool中使用的长度不一的,其实是为了能够把多个不满的本地空闲列表还给ResourcePool时节省空间,例如下面代码
class ResourcePool {
bool push_free_chunk(const FreeChunk& c) {
DynamicFreeChunk* p = (DynamicFreeChunk*)malloc(
offsetof(DynamicFreeChunk, ids) + sizeof(*c.ids) * c.nfree);
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;
}
};
返还整个空闲列表时是请求了一个动态的空闲列表,其大小等于这个空闲列表中实际存储的大小。
从池中请求对象
现在所有准备工作已经完成了,我们可以正式的来理解如何请求对象了。
由于我们申请对象时,可能有参数,也就是 类似于 new T(x); 亦或者没有参数,类似于 new T;注意后者是不同于new T(),因为加括号版本是会把整个对象置0。为了区别开这两者,我们使用宏来进行定义。
整个操作优先从空闲列表中取出对象,这减少了内存碎片。
#define 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; \
return unsafe_address_resource(free_id); \
} \
/* Fetch a FreeChunk from global. \
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; \
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; \
++_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; \
++_cur_block->nitem; \
return p; \
} \
return NULL;
template<class T>
inline T* LocalPool<T>::get(ResourceId<T>* id) {
RESOURCE_POOL_GET();
}
template<class T, class A>
inline T* LocalPool<T>::get(ResourceId<T>* id, const A& x) {
RESOURCE_POOL_GET((x));
}
id是个传出参数,我们来理解下整个操作流程:
- 首先检查本地的空闲列表是否为空,不为空则从中取出一个对象并返回。这个操作是线程本地的,不需要任何同步。
- 再检查ResourcePool中空闲列表的列表是否为空,不为空则取一个空闲列表放回本地,然后再从中取出对象返回。对空闲列表的列表进行写操作是需要加锁的,并且要再次判断这个列表是否为空(双重锁定),最后弹出来解锁再进行memcpy操作。
template<class T>
class ResourcePool {
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;
}
};
- 再检查本地的block是否是空的,不为空则从中取出一个并计算id,然后进行置换new操作。
- 最后,若本地的block也是空的,就从ResourcePool中取出一个Block,这将调用add_block函数。
template<class T>
class ResourcePool {
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(std::memory_order_acquire);
if (ngroup >= 1) {
BlockGroup* const g =
_block_groups[ngroup - 1].load(std::memory_order_consume);
const size_t block_index =
g->nblock.fetch_add(1, std::memory_order_relaxed);
if (block_index < RP_GROUP_NBLOCK) {
g->blocks[block_index].store(
new_block, std::memory_order_release);
*index = (ngroup - 1) * RP_GROUP_NBLOCK + block_index;
return new_block;
}
g->nblock.fetch_sub(1, std::memory_order_relaxed);
}
} while (add_block_group(ngroup));
// Fail to add_block_group.
delete new_block;
return NULL;
}
};
这个函数直接new一个Block,之后会把它给本地Pool,在此之前,需要先记录下这个Block。如何记录呢?首先观察_ngroup是不是0,是0我们就需要添加一个BlockGroup了;然后在最后一个group中,我们去看这个group满了没有,没有满就直接把他存进去,满了还是需要添加一个BlockGroup。
再来注意这个函数的同步操作,对_ngroup而言只有add_block_group会改变这个值,add_block_group()中的操作(例如将新group添加到_block_groups中)必须保证其已经执行才获得增加值,所以为memory_order_acquire。然后就是获得最后一个BlockGroup的指针,我们必须保证关于它的操作(例如new BlockGroup)已经执行,所以为memory_order_consume。然后我们直接原子的使得这个BlockGroup中的计数值加1,再根据这个index进行判断,在范围内就store下新的block,这里使用memory_order_release是保证之前的操作(例如new操作)在通过unsafe_address_resource等函数读区之前已经进行了。最后不成功原子的减一消除影响。这里一增一减之间安全性由block_index的唯一性保证(即若block_index在安全范围内,它必是唯一的,通过这个函数只能获得这个值一次!)。
再来看看add_block_group()函数,这个函数请求一个BlockGroup对象并放到数组中去。整个操作使用了互斥锁比较简单直接,不再赘述。
template<class T>
class ResourcePool{
static bool add_block_group(size_t old_ngroup) {
BlockGroup* bg = NULL;
pthread_mutex_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.
pthread_mutex_unlock(&_block_group_mutex);
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);
}
}
pthread_mutex_unlock(&_block_group_mutex);
return bg != NULL;
}
};
往池中返还对象
往池中返还对象相对简单直接。
template<class T>
class LocalPool{
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;
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;
return 0;
}
return -1;
}
};
如果本地空闲列表还未满,就简单的放入其中。
否则,将本地空闲列表返还给ResourcePool,然后将id加入新本地空闲列表中去。
template<class T>
class ResourcePool{
bool push_free_chunk(const FreeChunk& c) {
DynamicFreeChunk* p = (DynamicFreeChunk*)malloc(
offsetof(DynamicFreeChunk, ids) + sizeof(*c.ids) * c.nfree);
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;
}
};
以上就是ResourcePool的主体构成。