【项目】C++高并发内存池

一、项目介绍

        当前项⽬是实现⼀个⾼并发的内存池,他的原型是google的⼀个开源项⽬tcmalloc,tcmalloc全称 Thread-Caching Malloc,即线程缓存的malloc,实现了⾼效的多线程内存管理,⽤于替代系统的内 存分配相关的函数(malloc、free)。

tcmalloc源代码

        这个项⽬会⽤到C/C++、数据结构(链表、哈希桶)、操作系统内存管理、单例模式、多线程、互斥锁 等等⽅⾯的知识。

1、池化技术

  • 所谓池化技术,就是程序先向系统申请过量的资源,然后自己管理,当程序中需要申请内存时,不是直接向操作系统申请,而是直接从内存池中获取,释放内存时也不是将内存返回给操作系统,而是返回内存池中。

  • 因为每次申请该资源都有较大的开销,这样提前申请好了,使用时就会非常快捷,能够大大提高程序运行效率。

  • 在计算机中有很多使用这种池技术的地方,例如线程池、连接池等。

  • 以服务器上的线程池为例,它的主要思想是:先启动若⼲数量的线程,让它们处于睡眠状态,当接收到 客⼾端的请求时,唤醒池中某个睡眠的线程,让它来处理客⼾端的请求,当处理完这个请求,线程⼜进⼊睡眠状态。

2、主要解决的问题

        内存碎⽚分为外碎⽚和内碎⽚。外部碎⽚是⼀些空闲的连续内存区域太⼩,这些内存空间不连续,以⾄于合计的内存⾜够,但是不能满⾜⼀些的内存分配申请需求。内部碎⽚是由于⼀些对⻬的需求,导致分配出去的空间中⼀些内存⽆法被利⽤。

3、malloc

  • C++中动态申请内存都是通过malloc去申请的,但实际上我们并不是直接去堆中获取内存的,而malloc就是一个内存池。
  • malloc() 相当于向系统 “批发” 了一块较大的内存空间,然后“零售” 给程序使用,当全部使用完或者程序有大量内存需求时,再根据需求向操作系统申请内存。

⼀⽂了解,Linux内存管理,malloc、free 实现原理

二、定长内存池设计

1、开辟内存

  • 使用malloc开辟一大块内存,让_memory指针指向这个大块内存
  • _memory 设置为char* 类型,是为了方便切割时_memory向后移动多少字节数。

2、申请内存

  • 将_memory强转为对应类型,然后赋值给对方,_memory指针向后移动对应字节数即可。
  • 如果有存在已经切割好的小块内存,则优先使用小块内存。

3、释放内存

  • 用类型链表的结构来进行存储。
  • 用当前小块内存的头4字节存储下一个小块内存的地址,最后用_freeList指针指向第一个小块内存的地址(并不是将内存释放给操作系统)
  • 所以开辟内存时,开辟的内存大小必须大于或等于一个指针类型的大小。

4、向堆申请页为单位的大块内存

5、代码实现

#ifdef _WIN32		//如果32位则包含头文件
    #include<windows.h>
#else
    //
#endif

//定长内存池
//template<size_t N>
//class ObjectPool
//{};

// 直接去堆上按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
	//         参数: 分配的区域的起始地址,区域的大小(字节),分配的类型,内存保护
	void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
	// kpage << 13 以页为单位进行内存分配
#else
	// linux下brk mmap等
#endif

	if (ptr == nullptr)
		throw std::bad_alloc();

	return ptr;
}

template<class T>
class ObjectPool
{
public:
	T* New()
	{
		T* obj = nullptr;
		//优先利用还回来的内存
		if (_freeList)
		{
			void* next = *((void**)_freeList);	//转为void**类型,变为下一个结点的指针
			obj = (T*)_freeList;
			_freeList = next;
		}
		else {
			//剩余内存不够一个大小时重新开大块空间
			if (_remainBytes < sizeof(T))
			{
				_remainBytes = 128 * 1024;
				//_memory = (char*)malloc(_remainBytes);
				_memory = (char*)SystemAlloc(_remainBytes >> 13);  //_remainBytes >> 13 单位从字节数转换为页数
				if (_memory == nullptr)
				{
					throw std::bad_alloc();
				}
			}

			obj = (T*)_memory;
			//根据编译器确定指针类型大小
			size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
			_memory += objSize;			//后移一个指针的大小指向实际的内存块
			_remainBytes -= objSize;	//实际内存大小
		}
		//定位new,显示调用T的构造函数初始化
		new(obj)T;

		return obj;
	}

	void Delete(T* obj)
	{
		//显示调用析构函数
		obj->~T();

		if (_freeList == nullptr)
		{
			_freeList = obj;
			//*(int*)obj=nullptr;
			*(void**)obj = nullptr;
		}
		else {
			//头插
			*(void**)obj = _freeList;
			_freeList = obj;
		}
	}

private:
	char* _memory = nullptr;//指向大块内存的指针
	size_t _remainBytes = 0;//剩余字节数
	void* _freeList = nullptr;//还回来链表指针
};

三、高并发内存池整体设计

1、thread cache(线程缓存)

  • 每个线程独享一个thread cache,用于小于256k的内存分配情况,线程从这里申请内存不需要加锁(因为其他线程无法访问当前线程的 thread cache,没有竞争关系),这也就是这个并发线程池⾼效的地⽅。

2、central cache(中心缓存)

  • 所有线程共享一个central cache,thread cache 是按需从central cache中获取对象,central cache在合适的时候会收回thread cache中的对象,避免一个线程占用太多资源。
  • central cache 是所有线程共享的,所以存在竞争关系,需要加锁;这里使用的锁是桶锁,并且因为只有thread cache没有内存时才会申请内存,所以这里竞争不会太激烈。

3、page cache(页缓存)

  • 页缓存存储的内存是以页为单位进行存储的及分配的。central cache没有内存时,则会从page cache中申请一定数量的page,并切割成定长大小的小内存块。
  • 当一个span的几个跨度页的对象都回收后,page cache会回收central cache满足条件的span对象,并且合并相邻的页,组成更大的页,缓解内存碎片的问题。

四、thread cache 设计思路

1、线程申请内存

  • 当内存申请size<=256KB,先获取到线程本地存储的thread cache对象,计算size映射的哈希桶自由链表下标i。
  • 如果自由链表_freeLists[i]中有对象,则直接Pop⼀个内存对象返回。
  • 如果_freeLists[i]中没有对象时,则批量从central cache中获取⼀定数量的对象,插⼊到自由链表并返回⼀个对象。
  • 线程申请内存时,会有不同规格的内存申请(4字节、5字节等),根据范围划定不同的自由链表,设计多个自由链表管理不同规格的内存小块。实质就相当于使用多个定长内存池的自由链表。

2、线程对齐规则

  • 每个内存小块采用向上对齐原则(可能会出现多申请内存的情况,这就是内碎片)
  • 例如:

    • 需要9字节,则开辟一个大小为2个8字节的空间的节点        
    • 需要100字节,则开辟一个大小为13个8字节的空间的节点。
  • 整体控制在最多10%左右的内碎片浪费
  • 总计设计208个桶
  • freelist[0,16)个桶,采用8byte对齐,内存大小[1,128]
  • freelist[16,72)个桶,采用16byte对齐,内存大小[128+1,1024]
  • freelist[72,128)个桶,采用128byte对齐,内存大小[1024+1,8*1024]
  • freelist[128,184)个桶,采用1024byte对齐,内存大小[8*1024+1,64*1024]
  • freelist[184,208)个桶,采用8*1024byte对齐,内存大小[64*1024+1,256*1024]

  • 注意:_freeLists是一个数组,每个元素都是自由链表类型(即存储自由链表的头结点)

3、线程释放内存

  • 释放内存后:当释放内存⼩于256k时将内存释放回thread cache,计算size映射⾃由链表桶位置i,将对象Push 到_freeLists[i]。(每一个切分好的小块内存就是一个节点)
  • 具体方法是:用切分好的小块内存的前4字节或8字节来存储下一个小块内存的地址。
  • 插入节点时,采用头插的方式。
  • 当链表的⻓度过⻓,则回收⼀部分内存对象到central cache。

4、TLS--thread local storage

  • TLS:thread local storage 线程本地存储(linux和Windows下有各自的TLS)
  • TLS是一种变量的存储方式,这个变量在它所在的线程内是全局可访问的,但是不能被其他线程访问,这样就保证了线程的独立性。
  • 静态TLS使用方法:

    • _declspec(thread) DWORD data=0;
    • 声明了一个 _declspec(thread) 类型的变量,会为每一个线程创建一个单独的拷贝。
  • 原理:

    • 在x86 CPU上,将为每次引用的静态TLS变量生成3个辅助机器指令
    • 如果在进程中创建子线程,那么系统将会捕获它并且自动分配一另一个内存块,以便存放新线程的静态TLS变量。

linux gcc下 tls

// TLS thread local storage
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
//_declspec(thread)声明线程局部存储,每个线程都有自己独立的 pTLSThreadCache 变量的副本

//

static void* ConcurrentAlloc(size_t size)
{
	//通过tls,每个线程无锁获得自己专属的threadCache
	if (pTLSThreadCache == nullptr)
	{
		pTLSThreadCache = new ThreadCache;
	}
	cout << std::this_thread::get_id() << ":" << pTLSThreadCache << endl;
	return pTLSThreadCache->Allocate(size);
}

五、central cache 设计思路

1、central cache 结构

  • central cache也是一个哈希桶结构,每个哈希桶位置挂载的是SpanList自由链表结构。
  • Span管理的是以页为单位的大块内存(一页为8kb(32位系统下))
  • 每个Span中的大内存根据映射关系被切成了一个个的小块内存对象,然后挂载在Span上。
  • 因为中心缓存是所有线程共享的,只需要定义一个对象,所以这里需要将 central cache 设计为单例模式(这里采用饿汉模式的设计方法)
  • 注意:

    • span是双向链表,而span下挂载的小块内存对象是单链表。
    • 中心缓存需要加桶锁。
    • _spanLists 是一个数组,数组中每个元素都是一个span自由链表的_head头指针。
    • 每个span又是一个单向自由链表。

2、内存申请

  • 当thread cache中没有内存时,就会批量向central cache申请一些内存对象,从对应的span中取出小块内存对象给thread cache,这个过程是需要加锁的(加桶锁)
  • 这里批量获取对象的数量使用了类似网络tcp协议拥塞控制的慢开始算法。
  • 如果所有的span都为空了(即central cache使用完了),则将空的span链在一起,向page cache申请一个span对象,span对象中是一些以页为单位的内存,需要切成对应的小块内存对象,并链接起来,挂到span中。
  • central cache中每一个span都有一个use_count,分配一个对象给thread cache,就加加。

3、内存释放

  • 当thread_cache将内存释放回central cache中的时,释放回来就减减use_count。
  • 当use_count减到0时则表示所有对象都回到了span,则将span释放回page cache,page cache中会对前后相邻的空闲页进行合并。

六、page cache设计思路

  • page cache中也是哈希桶结构,但是每个节点存储的都是span
  • 因为页缓存是所有线程共享的,只需要定义一个对象,所以这里将 page cache 设计为单例模式(这里采用饿汉模式的设计方法)
  • 注意:page cache需要加整体锁(因为是所有线程共享的)

1、申请内存

  • 当central cache向page cache申请内存时,page cache先检查对应位置有没有span,如果没有则 向更⼤⻚寻找⼀个span,如果找到则分裂成两个。⽐如:申请的是4⻚page,4⻚page后⾯没有挂 span,则向后⾯寻找更⼤的span,假设在10⻚page位置找到⼀个span,则将10⻚page span分裂 为⼀个4⻚page span和⼀个6⻚page span。
  • 如果找到_spanList[128]都没有合适的span,则向系统使⽤mmap、brk或者是VirtualAlloc等⽅式申请128⻚page span挂在⾃由链表中,再重复1中的过程。
  • 需要注意的是central cache和page cache 的核⼼结构都是spanlist的哈希桶,但是他们是有本质区 别的,central cache中哈希桶,是按跟thread cache⼀样的⼤⼩对⻬关系映射的,他的spanlist中挂的span中的内存都被按映射关系切好链接成⼩块内存的⾃由链表。⽽page cache 中的spanlist则是按下标桶号映射的,也就是说第i号桶中挂的span都是i⻚内存。

2、释放内存

如果central cache释放回⼀个span,则依次寻找span的前后page id的没有在使⽤的空闲span, 看是否可以合并,如果合并继续向前寻找。这样就可以将切小的内存合并收缩成⼤的span,减少内 存碎⽚。

七、申请过程

1、类和对象定义

FreeList对象

ThreadCache对象

ObjectPool对象

CentralCache对象

Span对象

SpanList对象

PageCache对象

2、每个线程获得专属ThreadCache对象

申请6b大小的内存空间

如果小于256k,线程获得自己的ThreadCache对象,利用ObjectPool定长内存池New出一个ThreadCache对象tcPool,然后Allocate分配内存。

new也是封装了malloc来分配内存,使用ObjectPool来替换new,彻底不用malloc。

New对象时,构造_freeList为空,申请128k大小的内存,obj指向分配的内存(一个ThreadCache),_memory指向分配后的内存块。

直接在堆上申请空间,返回ptr。

3、Allocate计算对齐大小和所属哈希桶

分配到pTLSThreadCache线程后再去Allocate(6b)

RoundUp计算对齐数,6b大小则对齐到8b字节

Index计算在ThreadCache哪个桶中,6b大小在第0个桶中

4、慢反馈算法获得要申请对象的数量

项目刚开始_freeList[index]为空,去CentralCache申请内存。_freeLists[index]的maxsize为空,通过慢反馈获得要申请小对象的数量,如果batchNum为maxsize,则申请小对象数量为1。

慢反馈调节,MAX_BYTES为256k,申请字节越大,返回越少

5、从CentralCache中获得对象

定义start和end,通过FetchRangeObj从CentralCache对应哈希桶中获得对象,如果不够则有多少获得多少,数量为actualNum。大于1的话将多余的对象插入_freeList[index]中。

返回内存起始地址start,一次内存申请完成。

计算size(8)所在的哈希桶,给对应_spanLists[index]加锁,然后获取span对象

依次判断桶中的各个span有无小对象,如果有则返回头结点。

6、从PageCache中获取对象 

如果CentralCache对应哈希桶中没有小对象,则去PageCache申请一个span大的空间,将span标记为已使用。

慢反馈NumMovePage,size为8b时,通过NumMoveSize慢反馈获得num为512,npage为4096,右移13位为0,申请1页。

NewSpan获取1页的span,先判断PageCache的桶中有没有span,如果有则_spanLists[k]头删一个kspan并返回。

如果PageCache的桶中有没有span,检查后面的桶的span,申请一个1页的span,如果有一个10页的span,则切分为一个1页的kSpan和一个9页的nSpan,返回kSpan。

如果PageCache中没有Span,则去堆上申请一个128页的内存,放在bigSpan中,将bigSpan头插到第128个_spanLists中,然后再执行一遍NewSpan返回需要的span。

7、切分申请好的span

PageCache申请一个span大的空间后将span进行切分,计算起始地址和结束地址

 size为切分小对象的大小,tail的nextObj等于start,依次切分,计算这个span能分多少个小对象i。

 再将span头插到CentralCache的_spanLists[index] 桶中,返回span。

8、从span中取batchNum个对象

从_spanLists[index]中获取了span,start为_freeList头,将end递增到batchNum个对象,之后的内存接到_freeList中,返回实际获得的对象数量actualNum。

八、释放过程

1、获得对象到span的映射

释放获取的p1内存(8b)

获得从对象到span的映射,得到释放对象的字节大小(8b),该线程调用Deallocate释放。

将对象地址强转为PAGE_ID(size_t)类型再右移13位得到对象的id,通过id找到PageCache中分出去对应的span,并返回该span。

2、将对象插入自由链表桶

获取Index桶号,将对象Push到对应_freeLists[index]中,释放完成。判断_freeLists[index]大于申请时候的值MaxSize,说明申请的内存都还回来了,则统一释放给CentralCache。

当链表过长时释放链表到CentralCache对应_spanLists[index]中。先弹出所有的小对象存到start和end中,再通过start和大小释放给CentralCache。

3、释放对象到CentralCache中

通过size(8b)获得Index所在的桶,将_spanLists[index]加锁便于插入对象,当start还有对象时,获取下一个对象NextObj,获取start指向的对象的span映射,将start头插到span的_freeList链表中,span分出去的小对象计数_useCount--,start后移到下一个小对象,然后继续头插。直到将所有小对象插入后解开桶锁。

如果_useCount为0说明所有小对象都回来了,将span从该桶中摘除,指针置空。然后PageCache加锁,将该span还给PageCache。

4、释放对象到PageCache

先获取span的页号,-1获得前一个页的页号prevId,通过prevId得到前一个span,如果前一个span没有使用,则与当前的span进行合并,将 span的起始页调整为前一个页段的起始页,并更新页数。然后,从_spanLists中删除prevSpan,在_spanPool中调用delete删除prevSpan。

然后获得下一个span的_pageId,检查下一个span是否没有使用,通过nextId返回对应的Span,合并span的_n页数,将nextSpan在_spanLists中摘除,通过_spanPool调用delete释放nextSpan。

PageCache中的_spanLists中插入还回来的span,标记为未使用状态,建立_pageId和span的映射。

九、大块内存的申请释放

1、申请过程

申请257k大的空间

如果申请的size大于MAX_BYTES(256k),计算对齐大小alignSize为264k,kpage为33页。然后PageCache加锁,直接从PageCache获取对应的内存,返回内存地址。

申请33个页,如果对应的桶中有span,则弹出并返回span,如果不够则查看后面的桶,有则进行切分。否则去堆中申请一个128页的span放在_spanLists[128]中。

如果申请的页大于128,则直接向堆申请,建立_pageId和span的映射,返回该span。

2、释放过程

释放257k的空间

先通过该指针获取所在的span,得到该内存的大小,如果size大于MAX_BYTES(256k),释放到PageCache中。

先对前后页进行合并,再插入到对应的_spanLists中,重新建立映射。

如果释放的内存大于128页,直接还给堆。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值