C++实现高并发内存池

1. 需求分析

池化技术是计算机中的一种设计模式,内存池是常见的池化技术之一,它能够有效的提高内存的申请和释放效率以及内存碎片等问题,但是传统的内存池也存在一定的缺陷,高并发内存池相对于普通的内存池它有自己的独特之处,解决了传统内存池存在的一些问题。

1.1 直接使用new/delete、malloc/free存在的问题

new/delete用于c++中动态内存管理而malloc/free在c++和c中都可以使用,本质上new/delete底层封装了malloc/free。无论是上面的那种内存管理方式,都存在以下两个问题:

  • 效率问题:频繁的在堆上申请和释放内存必然需要大量时间,降低了程序的运行效率。对于一个需要频繁申请和释放内存的程序来说,频繁调用new/malloc申请内存,delete/free释放内存都需要花费系统时间,频繁的调用必然会降低程序的运行效率。
  • 内存碎片:经常申请小块内存,会将物理内存“切”得很碎,导致内存碎片。申请内存的顺序并不是释放内存的顺序,因此频繁申请小块内存必然会导致内存碎片,造成“有内存但是申请不到大块内存”的现象。

1.2 定长内存池的优点和缺点

针对直接使用new/delete、malloc/free存在的问题,定长内存池的设计思路是:预先开辟一块大内存,程序需要内存时直接从该大块内存中“拿”一块,提高申请和释放内存的效率,同时直接分配大块内存还减少了内存碎片问题。

  • 优点:简单粗暴,分配和释放的效率高,解决实际中特定场景下的问题有效。
  • 缺点:功能单一,只能解决定长的内存需求,另外占着内存没有释放。

对于STL中的空间配置器就是采用的这种方式,当申请小于128字节的内存就是用定长内存池,当超过时,就直接使用malloc和free
在这里插入图片描述

定长内存时详解链接:https://blog.youkuaiyun.com/MEANSWER/article/details/118343707

1.3高并发内存池要解决的问题

基于以上原因,设计高并发内存池需要解决以下三个问题:

  • 效率问题
  • 内存碎片问题
  • 多线程并发场景下的内存释放和申请的锁竞争问题

2. 总体设计思路

高并发内存池整体框架由以下三部分组成,各部分的功能如下:

  • 线程缓存(ThreadCache)每个线程独有线程缓存,主要解决多线程下高并发运行场景线程之间的锁竞争问题。线程缓存模块可以为线程提供小于64k内存的分配,并且多个线程并发运行不需要加锁。线程从这里申请内存不需要加锁,每个线程独享一个ThreadCache,这也就是这个并发内存池高效的地方(这就是tcmalloc名字的本质来源,在这里具体的实现采用的是TLS(thread local storage 本地线程存储,可以理解为每个线程独有的全局变量,但是是本地的)))。(本质上ThreadCache里面就是由hash映射的定长的内存桶
  • 中心缓存(CentralCache):中心缓存是所有线程所共享,thread cache是按需从central cache中获取的对象。central cache周期性的回收thread cache中的对象,避免一个线程占用了太多的内存,而其他线程的内存吃紧。达到内存分配在多个线程中更均衡的按需调度的目的。central cache是存在竞争的,所以从这里取内存对象是需要加锁,(比如说一个线程Thread Cache下的8字节映射的自由链表过长,就需要还回Central Cache,但是此时另外的一个线程Thread Cache下8字节的自由链表没有了,就需要向Central Cache要,此时就需要加锁,因为这种情况很少会出现)不过一般情况下在这里取内存对象的效率非常高,所以这里竞争不会很激烈。
  • 页缓存(PageCache):以页为单位申请内存,为中心控制缓存提供大块内存。当中心控制缓存中没有内存对象时,可以从page cache中以页为单位按需获取大块内存,同时page cache还会回收central control cache的内存进行合并缓解内存碎片问题。

在这里插入图片描述

注:怎么实现每个线程都拥有自己唯一的线程缓存呢?

3. 申请流程

我们的并发内存池项目对外只暴露两个接口,对于申请的接口就是ConcurrentAlloc(),如果申请的内存大于64KB,直接走的就是PageCache所提供的的NewSpan(),但是走这个也有可能有两种情况,一种是介于16页——128页之间,一开始就会直接的申请上来一块128页的内存然后进行切分,返回你需要的那一部分。大于128页直接调用VirtualAlloc进行向系统申请内存。如果是小于64KB,那么就会走ThreadCache所提供的Allocate()接口,计算出要找哪一个索引下标的freelist,如果有就直接返回,没有则会调会FetchFromCentralCache(),通过你要的内存size计算出需要给你返回的批量个数(慢启动方式)以及实际上真正能给你返回的数量调用CentralCache的FetchRangeObj(),但是有可能CentralCache中的SpanList[i]下没有Span或者内存都被用完了,所以首先就是得到块有内存的Span,调用GetOneSpan()接口,如果该Span中的list不为nullptr说明还有内存,如果为空就需要向PageCache要一块Span,调用NewSpan()接口。计算索引看PageCache下是否有合适的页,如果没有则需要向后找,在没有就只能向系统直接申请一块128页的内存,然后进行切分了。由于CentralCache中的Span都是切好的,所以在得到这个新的页的时候,也应该按照对应的内存大小将他切分好然后在返回CentralCache

4. 释放流程

一块块内存还回来挂接在ThreadCache中对应的FreeList中,当其中一个FreeList挂接的太长的时候就需要进行归还给CentralCache(这里选择归还的条件就是自由链表中的内存个数Size大于MaxSize),从该FreeList中取出MaxSize个内存归还到CentralCache,但是每一块小内存都可能来自于不同的Span(根据每一块的小内存的起始地址算出它所对应的页号,然后还有一个map可以通过页号找到所对应的Span,那么就可以确保每一块小内存都归还给当初所切出来的Span中),在CentralCache中的每一个大块Span里面有一个usecount,如果为0的时候,说明分给ThreadCache的内存就都还回来了,那么为了能够合成更大的页,就需要再把该Span还回PageCache中,每一个大块的Span里面都有一个PageID(页号)和页数,那么就可以进行在PageCache中进行前后的搜索,找到是否还有大块的Span没有使用然后进行合并,成为更大的页。

5. 细节剖析

5.1 ThreadCache

ThreadCache的主要功能就是为每一个线程提供64K以下大小内存的申请。为了方便管理,需要提供一种特定的管理模式,来保存未分配的内存以及被释放回来的内存,以方便内存的二次利用。这里的管理通常采用将不同大小的内存映射在哈希表中,链接起来。而内存分配的最小单位是字节,64k = 102464Byte如果按照一个字节一个字节的管理方式进行管理,至少也得需要102464大小的哈希表对不同大小的内存进行映射。为了减少哈希表长度,这里采用不同段的内存使用不同的内存对齐规则,将浪费率保持在1%~12%之间。具体结构如下:
在这里插入图片描述
具体说明如下

  • 为了将内存碎片浪费保持在12%左右,这里使用不同的对齐数进行对齐。
  • 0 ~ 128采用8字节对齐,129 ~ 1024采用16字节对齐,1025 ~ 8 * 1024采用128字节对齐, 8 * 1024~64*1024采用1024字节对齐;内存碎片浪费率分别为:7/8,15/144,127/1152,1023/8 * 1024 + 1024均在12%左右(除了第一块按照8字节对齐的浪费率)。同时,8字节对齐时需要[0,15]共16个哈希映射;16字节对齐需要[16,71]共56个哈希映射;128字节对齐需要[72,127]共56个哈希映射;1024字节对齐需要[128,184]共56个哈希映射。
	static inline size_t _Index(size_t bytes, size_t align_shift)
	{
   
		return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;
	}

	// 计算映射的哪一个自由链表桶
	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
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值