ResourcePool

前面跟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的主体构成。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值