菜鸟笔记
提升您的技术认知

brpc 笔记

bthread(一) 前言bthread(二) 线程模型及bthreadbthread(三) bthread数据结构bthread(四) bthread用户接口和代码执行路径bthread(五) 无锁队列rq的代码实现bthread(六) 小结brpc的精华bthread源码剖析brpc介绍、编译与使用brpc源码解析(一)—— rpc服务添加以及服务器启动主要过程brpc源码解析(二)—— brpc收到请求的处理过程brpc源码解析(三)—— 请求其他服务器以及往socket写数据的机制brpc源码解析(四)—— Bthread机制brpc源码解析(五)—— 基础类resource pool详解brpc源码解析(六)—— 基础类socket详解brpc源码解析(七)—— worker基于ParkingLot的bthread调度brpc源码解析(八)—— 基础类EventDispatcher详解brpc源码解析(九)—— 基础类WorkStealingQueue详解brpc源码解析(十)—— 核心组件bvar详解(1)简介和整体架构brpc源码解析(十一)—— Reducer类和Adder类解析brpc源码解析(十二)—— 核心组件bvar详解 AgentGroup类详解brpc源码解析(十三)—— 核心组件bvar详解(4)combiner详解brpc源码解析(十四)—— 核心组件bvar详解 sampler详解brpc源码解析(十五)—— bthread栈创建和切换详解brpc源码解析(十六)—— 作为client的连接建立和处理详解brpc源码解析(十七)—— bthread上的类futex同步组件butex详解brpc源码解析(十八)—— MPSC队列ExecutionQueue详解brpc源码解析(十九)—— 双buffer数据结构DoublyBufferedData详解brpc源码解析(二十)—— 用于访问下游的Channel类详解

brpc源码解析(五)—— 基础类resource pool详解

阅读 : 770

brpc是一个完整的rpc框架,其中用到了很多比较优秀的基础类,会看情况分析下这些类的源码,从这篇的resource pool开始,resource pool是很重要的基础类之一,顾名思义,就是资源池,是一个用于在多线程环境下进行资源分配和回收的类,可以理解为高度竞争环境下速度更快的new和delete。resource pool在brpc里面有着大量的使用,比较典型的,socket的分配和bthread 的taskmeta分配都是用的resource pool,因为二者对于创建的速度有着很高的要求。而且因为需要支持各种类型,这个类也很好地诠释了c++模板的使用。

既然功能是资源分配,最常用的自然是分配和回收资源,直接供外部调用的函数如下:

get_resource和new类似,是获取对象的,这边提供了三个重载,分别对应没有参数、1个参数和2个参数构造对应类型对象的情况,而内部则是调用ResourcePool::singleton()->get_resource,注意这几个函数并不是类函数,不要和后面resource pool内部的get_resource弄混了,这里贴一个以前介绍过的socket create里实际的调用:

回到上面的ResourcePool::singleton()->get_resource,ResourcePool::singleton()是获取对应类型资源池的单例,具体的函数如下:

其中_singleton变量定义声明如下:


这是一个ResourcePool*类型的静态原子变量,也就是对每一种不同的T都有与之对应的变量,换句话说,就是每一种类型在调用resource pool后会有自己的资源池单例。而整个singleton()函数则是很常见的一个如果已经初始化了直接返回单例,没初始化则新建并返回单例的函数,因为是多线程调用所以用pthread_mutex_lock加了锁,并使用release-consume语义来保证某线程的新建对其他线程读取的可见性。

1.获取资源

获取到单例后,调用的是内部的ResourcePool单例内部的 get_resource,有如下几个重载:

这几个重载都是先调用get_or_new_local_pool()获取到一个LocalPool类型的指针lp,随后使用指针调用get,get_or_new_local_pool()函数如下:

_local_pool是一个thread local变量,也就是每个pthread会有一个,注意LocalPool这个类型,它是整个resource pool的资源分配的入口,构造的时候会传入全局的单例resource pool,_local_pool定义声明如下:


get_or_new_local_pool()里,首先判断_local_pool是否已经有了,如果有则直接返回,如果没有则新建后返回,新建成功后会调用thread_atexit登记一个pthread退出后删除_local_pool的函数,同时给_nlocal加一,该变量表明T类型的resource pool的local pool数量。
拿到local pool的指针lp后就会调用lp->get,local pool的三个get函数如下:

这三个重载都是一个BAIDU_RESOURCE_POOL_GET的宏定义,之所以要用宏定义来实现对不同个数参数的重载,注释里给了解释:

POD指的是Plain Old Data,基本数据类型、指针、union、数组、trivial构造函数的 struct或者 class都属于这类数据,这类数据属于C++中与C相兼容的数据类型,可以按照C的方式来处理(运算、拷贝等)。这里是想要调用new T而不是 new T()来避免不必要的memset,节省开销。
在介绍取Resource的机制前,先介绍下ResourceId,ResourceId是一个模板struct,如下:

包含一个uint64_t 的value,通过重载uint64_t运算符可以直接当uint64_t类型使用,还有一个模板函数可以实现不同类型的ResourceId转换,ResourceId是resource pool中某个资源的唯一标识,所有的资源获取和归还都是基于ResourceId的,比如获取资源就是resource pool返回资源并将资源id写入到传入的ResourceId里,在上面那个socket的例子里,就是slot变量,该变量会用于组成socket的版本。
resource pool在内存分配上是按块来的,Block是ResourcePool类里的一个struct,如下:

NItem是当前block里已建立的item的数量,初始为0,而items数组实质上则是提前分配好的内存。
Block则是受BlockGroup管理,BlockGroup初始化block数量为0,把blocks数组里的所有指针都初始化为NULL。

ResourcePoolFreeChunk是一个模板struct,顾名思义,是空闲的Chunk,里面有两个变量,空闲的资源个数,和id数组:

ResourcePool与之相关的两个typedef如下:

这两个的区别就在于,FreeChunk是固定大小为FREE_CHUNK_NITEM的一个chunk,而DynamicFreeChunk是利用柔性数组实现的一个变长的chunk,使用柔性数组可以节省内存,后面会详细展开解释。
在BAIDU_RESOURCE_POOL_GET宏定义里,依次按照如下几个步骤尝试去拿需要的资源,

1.本地已有空闲的资源和id,直接从block中找到资源返回

_cur_free是一个FreeChunk变量,也就是当前thread的local pool里的空闲资源chunk,nfree是该chunk空闲资源个数,如果大于0,则根据nfree在对应位置取出一个free_id,赋值给传进来的参数id,并且调用unsafe_address_resource去取出资源,unsafe_address_resource就是根据id算出位置,去相应的block group和里面的block取,如下:

除了unsafe_address_resource还有一个类似的address_resource函数,增加了一些合法性的判断,性能相对差点,用于给外部调用:

2.全局有空闲的资源和id

如果本地的chunk没有空闲资源,则看有没有全局的free chunk,_pool->pop_free_chunk(_cur_free)是从全局取一个free_chunk赋值给_cur_free,如果取到了,则进行和步骤1一样的操作。pop_free_chunk函数如下:

_free_chunks是resource pool的一个std::vector类型的类变量,DynamicFreeChunk前面说过了,是一个可变长度的freechunk,整个函数就是从_free_chunks尾部取一个freechunk拷贝给传进来的freechunk,注意这里拷贝的是空闲的resourceId,根据resourceId可以计算出资源所在块。

3.在本地block新建资源实例

如果_free_chunks为空,说明全局也没有已有的空闲的资源了,这个时候优先考虑从本地block上新建对象, _cur_block 是local pool里的Block类型的类变量。如果_cur_block里已有的item数量小于上限,则直接在里面新建一个对象,id->value指明了新建对象在clock里的位置,address会用到。注意T p = new ((T*)_cur_block->items + _cur_block->nitem) T CTOR_ARGS;这个语句,是在不分配内存的情况下指定位置直接新建对象,也就是在((T*)_cur_block->items + _cur_block->nitem)指明的内存空间上新建一个T类型的对象。

4.如果_cur_block没初始化或者已经满了,则先新建一个block把指针赋给_cur_block,在block里新建对象。

add_block函数如下:

2.归还资源

归还资源在外部是调用return_resource函数:

对应的resourcepool里的return_resource函数如下:

优先归还到本地的_cur_free,如果_cur_free满了,则把_cur_free push到全局的_free_chunks里,然后把当前归还的id放到_cur_free里。
push_free_chunk函数如下:

DynamicFreeChunk* p的内存分配就利用了柔性数组的特性,根据c的大小来进行内存分配,体现了Dynamic。push_free_chunk在当前线程退出local pool被析构的时候也要被调用:

这种情况下_cur_free.nfree是不确定的, DynamicFreeChunk的动态内存分配可以节省空间。

3.总结

resource pool使用block和block group来管理内存,对象都是在block里分配的,新建对象会根据块容量等算出一个resourceId,后续的获取和归还都用根据这个Id算出便宜量来定位到具体的资源位置。

resource pool全局只有一个单例,blockgroup和block都是resource pool单例层面的,而每个pthread会有一个thread local 的local pool,local pool有一个FreeChunk类型的_cur_free变量,保存着局部的空闲资源的id,resource pool里还有一个全局的_free_chunks变量,保存着全局空闲资源的id,Localpool还有一个Block*类型的_cur_block变量,一个指向resource pool某个block的指针。

对于获取资源,如果_cur_free还有空闲的对象资源Id,那么直接找到对应资源返回,否则看_free_chunks里有没有FreeChunk,有的话拷贝到_cur_free,然后取出空闲的对象资源Id,找到对应资源返回。如果局部和全局都没有已有空闲资源则优先在_cur_block上新创建,如果这个指针为null或者指向的block已经满了,则调用resource pool的add_block新建一个block并赋值给_cur_block,并在上创建对象。这里提到的创建对象都是在已有内存上创建,没有分配内存的过程,所以比较快。

对于归还资源,则是优先将id存在_cur_free,如果_cur_free满了则把_cur_free push到_free_chunks里,空_cur_free后再将当前id放入_cur_free里。

无论是获取还是归还都会有在局部空闲id列表和全局空闲id列表之间进行memcpy的情况,是比较耗性能的,但是因为仅仅是很小的resourceId数据的拷贝,消耗还是可接受的。总的来说,就是利用提前的块内存分配以及对象的重复利用,实现了高性能的资源分配。