自底向上brpc(一):resource_pool

2 篇文章 0 订阅

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下的对象位置。

注意

  1. 一个block只会被一个线程所使用,而block的分配是从全局的block_group中所分配,所以free_chunk所存的id一定是自己线程所释放的对象,但当free_chunk达到一定数量时,push进的free_chunks则是针对整个对象的,即全局的对象,所以pop的时候需要加锁。
  2. 被回收的对象并没有被真正释放,所以通过ResourceId是可以依然访问被释放的对象的,所以存在ABA的问题,需要用户在上层通过version技术去规避。

总结

brpc的资源池实现还是比较容易理解的,与我曾经看过的资源池大同小异。我自己印象比较深刻的点,一是brpc非常看重locality,通过thread local来实现高效的资源分配,并通过一个全局的blockgroup来进行分发blcok,再通过一个全局的freechunks来进行回收object,实现资源的合理调度。同时为了防止多线程下分发blcok导致的锁竞争导致的性能下降,采取了lock-free的实现方式。所以虽然能看明白大概的原理,但如果要抠lock-free的实现细节,其实还是比较复杂的,每一个原子变量的参数该怎么设置,如何避免各种情况下的多线程竞争,这需要作者具有很强的并发编程能力,这也是我所需要学习的。但这片文章的目的是弄明白资源池的原理,所以就先止步于此吧!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值