高并发内存池的实现

首先看下结果:
在这里插入图片描述
在模拟高并发场景下,本次实现组件的申请释放内存效率,相比较malloc而言,提升性能较为乐观。

业务逻辑模型:

在这里插入图片描述

threadcache:这个是每个线程独享的内存池,不存在临界资源争夺问题,如果线程申请的内存小于你所限定的值,逻辑就直接往这里走,如果说大与阈值,则会向pagecache去申请,那样会导致线程安全问题,但是考虑到在实际使用中,系统不会频繁得去申请大块内存,所以这块不会因为总是释放和申请锁而浪费大量资源。

centralcache:该缓存池只存在一个,存在线程安全问题。假设上级一个threadcache内部的某个哈希桶链太长了,那么此时就会往centralcache中返回,从而可以给其他线程的threadcache使用;如果threadcache某个哈希位置的桶不够用了,会向centralcache申请,同时这个申请也不是需要多少拿多少,会多给,因为这次操作是非线程安全的,效率比较低,多给点,以后别频繁来要,起到居中调度的作用。
pagecache:此层缓存充当最重要的角色,是它和操作系统交互,向os申请空间,来为整套业务服务的。它在向os申请空间时,一次会申请大页分给centralcache的请求,剩下的挂到自己对应位置的hash桶中。每次centralcache来申请,pagecache至少是给它一整页的,具体按照centralcache的申请大小来看,如果是小的内存,给它一页,它拿回去切段也能切很多,如果是大内存,就给较多的页。包括释放流程,pagecache也承担合并连续的页释放给os的重要责任。

threadcache中的数据结构:

在这里插入图片描述

相关技术参考tls线程本地存储

计算映射哪个桶

	static inline size_t Index(size_t bytes)
	{
		assert(bytes <= MAX_BYTES);
		//每个区间有多少个链
		static int group_array[4] = { 16, 56, 56, 56 };
		if (bytes <= 128){
			return _Index(bytes, 3);
		}
		else if (bytes <= 1024){
			return _Index(bytes - 128, 4) + group_array[0];
		}
		else if (bytes <= 8192){
			return _Index(bytes - 1024, 7) + group_array[1] + group_array[0];
		}
		else if (bytes <= 65536){
			return _Index(bytes - 8192, 10) + group_array[2] + group_array[1] + group_array[0];
		}

		assert(false);

		return -1;
	}

threadcache通过慢启动向centralcache申请内存段数量

void* ThreadCache::FetchFromCentralCache(size_t i, size_t size)
{
	//获取一批对象(一批span),慢启动
	//去中心缓存获取batch_num个对象
	size_t batchNum = min(SizeClass::NumMoveSize(size), _freeLists[i].MaxSize());

centralcache的数据结构:hash桶对应位置的span只能按对应位置的内存大小切分
在这里插入图片描述

pagecache的数据结构:
在这里插入图片描述

pagecache应对对应大小页,桶没有,从而往下寻找切分分配的处理方法

//走到这里是因为k+1大小的页没有,所以要从k+1的映射位置开始切割
for (size_t i = k + 1; i < NPAGES; i++){
	//把大页切小,以span返回
	// 切出i-k页挂回自由链表
	if (!_spanList[i].Empty()){
		//换成尾切
		Span* span = _spanList[i].PopFront();
		Span* split = new Span;
		split->_pageId = span->_pageId + span->_n - k;
		split->_n = k;
		// 改变切出来span的页号和span的映射关系
		{	
			for (PageID i = 0; i < k; ++i){
				_idSpanMap[split->_pageId + i] = split;
			}
		}

		span->_n -= k;
		//split是需要的,span是多出来剩下的,这里把它嫁接到还剩多少页的i位置
		_spanList[span->_n].PushFront(span);
		return split;
	}

以下为争对第一次申请或者申请页数超过了NPAGES的方法:

//针对一下子申请超过128页的操作,直接找系统要
if (k >= NPAGES){
	void* ptr = SystemAllocPage(k);
	Span* span = new Span;
	span->_pageId = (ADDRES_INT)ptr >> PAGE_SHIFT;
	span->_n = k;
	{	
		_idSpanMap[span->_pageId] = span;
	}
	return span;
}

相关技术参考:virtualalloc
相关技术参考:brk

以下为pagecache切分大页时的细节
在一开始pagecache向系统申请最大页的时候,简称为一段大span,在此时,里面的所有pageid都与该span建立映射关系;我们在后续切分使用的过程中采用尾切的原因如下:为了减少映射的重新建立。如果我们采用头切,那么之前bigspan的大多数页,都要与新span重新建立关系,而使用尾切,可以保持大多数pageid与原来bigspan的映射关系不变,而只让新span与申请的小页建立新映射。(考虑通常情况下切小页的概率大)

//走到这里是因为k大小的页没有,所以要从k+1的映射位置开始切割
	for (size_t i = k + 1; i < NPAGES; i++){
		//把大页切小,以span返回
		// 切出i-k页挂回自由链表
		if (!_spanList[i].Empty()){
			//换成尾切
			Span* span = _spanList[i].PopFront();
			Span* split = new Span;
			split->_pageId = span->_pageId + span->_n - k;
			split->_n = k;
			// 改变切出来span的页号和span的映射关系
			{	
				for (PageID i = 0; i < k; ++i){
					_idSpanMap[split->_pageId + i] = split;
				}
			}

			span->_n -= k;
			//split是需要的,span是多出来剩下的,这里把它嫁接到还剩多少页的i位置
			_spanList[span->_n].PushFront(span);
			return split;
		}
	}

申请流程
1.threadcache->Allocate:如果申请的内存大与MAX_BYTES,则直接去向pagecache申请。如果不构成,第一步就先去找该thread的threadcache,该级缓存的结构就是一个开散列的hash桶。并且通过了tls技术,避免了在该位置加锁,每个线程并行访问自己的threadcache,提升了效率。(该哈希桶控制浪费在12%,使用分段映射)通过申请的size来计算应该访问freelists的哪个桶,如果这个桶不为空,就头删(弹出)一个内存块返回,否则就继续向centralcache去申请(同步使用慢启动机制)。如果返回>1个,则返回一个,剩下的挂在对应的freelists位置。 需要注意的:从道理上来说,centralcache对span没有切分的义务,应该交给threadcache自己去切,因为threadcache的操作是线程安全的,centralcache全局只有一个,如果让他切,会拉低整体的效率,其他线程需要等待。但是考虑后续释放的流程,需要一路向下合并,返回多余内存给系统,所以这里我们还是采用了让centralcache切的办法。
在这里插入图片描述
走到这里,threadcache已经在centralcache对应hash桶的spanlist中寻找猎物了,如果这个位置的hash桶所有span成员里面都没内存了,全部被用完,那么就要继续向下去找pagecache去要。回到上面,这里如果找到了一个span,进入猎物内部去数内存小段的个数,如果我此次申请的数量,这个span的剩余库存可以满足我,那么我可以直接取走;但是如果说我需要的数量大与库存了,该怎么办?这里我们采用有多少给多少的方法,不再扩展业务难度了…因为退一万步讲,threadcache这里说是说需要多个,但是它是为了后续使用方便,实际上它的买家也只是需要一个而已,我们把最起码的一个给它就可以了。在上述centralcache的spanlists没有合适span给予threadcache的情况下,centralcache会向pagecache去要,同时要回来的时候,centralcache会第一时间切分好,再返回。
关于threadcache在入口出申请的内存如果超过Max_Bytes,它会找pagecache去要,同时因为pagecache分配内存时需要按照页的倍数来,所以这里需要按照页大小对齐,并且如果这次申请的内存如果大于128页,则需要直接去向系统申请。

释放流程
释放流程也是从thread的入口进入,thread调用hreadcache->Dellocate。如果释放的大小不大于MAX_BYTES,则找到对应该大小的threadcache中对应的桶位置,头插到链表中,如果大与MAX_BYTES则直接还给pagecache。其中当hash桶中某个位置的链表长度>MaxSisze时,threadcache还会头删一段内存 回收到中心缓存。这一段内存中的每个小内存可能对应不同的span,他们会回到自己的span中,等到该span的usecount == 0,就说明这段span已经全部归还了。但是如何把打算归还的内存合理安置到他们各自对应的span中去呢?这里map<PageID,Span*>起到了重要作用!从threadcache归还内存给centralcache时候,是多段小内存,还有一个start参数,可以用start来表示每段小内存,start为void*,也就是空间地址,可以将它>>page_shift算出它的页号,根据map<pageid,span*>找出它是属于哪个span,将它链接到对应的span中去,同时_usecont–。接着继续start+一小段内存的大小,找到下一个小内存,重复操作。当一个span的_usecount == 0时,表示该span已经回收全部内存,可以归还给pagecache。(前面说了,一个span起码是一页,当时是这样分出去的!)
在pagecache中,会搜索前后的空闲页,如果存在,并且前一个页也是空闲的(一开始以没加_usecount==0的条件出错了,后来想起来是一自己忽略了在pagecache中也存在分页_usecount++的问题),就会把两个span合并,合成更大的span。向前合并与向后合并前都需要判断两者大小相加是否大与等于NPAGES,成立就不继续合并了,因为我们pagecache只有最大NPAGES-1的页桶。
关于页号,我们这里只考虑32位机器,4K为一页,一般64位机器,一页需要定义得大一点,不然页号的max会很大很大。

使用单例模式来使用centralcache和pagecache
将类的拷贝构造只声明不定义,并且声明成私有。(如果不私有别人可以在类外声明)
构造私有。并且定义私有的自己类型对象。并且定义共有的成员函数来获取这个实例对象,返回它的指针或者地址。
然后这个static的实例在testcpp里,不在声明它的.h文件里定义。全局访问的对象都是这个testcpp里定义的对象。

mutex锁的添加:
centralcache:在每个哈希散列的位置使用lock_guard(智能指针)加锁,8字节加锁,16字节加锁…
pagecache:需要加一把大锁,把整个pagecache的使用锁起来。
map<pageid,span*>:这里需要在各个使用map的地方加Mutex锁。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值