C++高并发内存池实战:CentralCache层

本文介绍了高并发内存池中CentralCache层的设计与实现。首先分析了Span对象的结构,包含页号、页数、前后指针等成员。然后设计了SpanList双向链表类,实现插入删除等操作,并采用单例模式创建CentralCache类。重点实现了三个关键函数:FetchRangeObj()用于分配内存给ThreadCache层,GetOneSpan()从PageCache层申请新内存,ReleaseListToSpans()管理ThreadCache层释放的内存。整个过程需要考虑多线程安全问题,通过加锁机制避免竞

作者
WildPointer
发布
2024.05.21
专栏
C++高并发内存池实战
阅读
约 10 分钟 / 935 次原文浏览
C++高并发内存池实战:CentralCache层

前言

在续《C++高并发内存池实战:ThreadCache层》文章分布后,这篇文章将着重对CentralCache层进行讲解,并实现ThreadCache层中所未实现的函数


Span对象

        在了解CentralCache层之前,我将对存在于CentralCache层和PageCache层的哈希桶内的Span类型进行讲解。首先为什么要设计Span类型?因为CentralCache层需要存储一个形似于ThreadCache层的链表,且CentralCache层的链表需要设计为双向链表(PS:为方便遍历寻找空闲的地址进行分配,所以设计为双向链表。由于单链表已经满足ThreadCache层管理内存的目的,再增加指针会加大分配的内存空间),所以Span对象应该存在两个指针,分别指向上一个节点和下一个节点的指针。在此基础上,用于CentralCache层所接收的Page层分配的内存大小都以页为单位,所以需要定义一个变量存储页号。且每一个Span用于下标不同,所以指向的分配内存也会不同,当一个Span指向的内存大于一页时,我们还需要定义存储页数的变量,通过这两个变量我们还可以快速定位内存的区间。用于Span管理的是多块内存,所以需要使用一个链表来存储管理的内存,还需要一个变量存储已经分配的内存块数量,故Span类型的设计如下:

cpp
//由分析而设计的Span结构体struct Span{	PAGE_ID _pageId = 0;	  //起始页的页号	size_t _n = 0;			  //页的数量	Span* _next = nullptr;    //指向上一个Span节点	Span* _prev = nullptr;    //指向下一个Span节点	size_t _useCount = 0;	  //已经被分配出去的内存数量	void* _freeList = nullptr;//管理内存的自由链表};

为什么通过设计的_pageId和_n变量可以快速定位内存的区间?例如已知_pageId为100,而该Span的页的数量为1,那么该Span的内存区间为100*8kb到101*8kb(假设一页为8kb)。针对上述代码,也可以通过下图直观的了解到Span类型的结构:

图1.Span结构详解图


CentralCache层

        根据上一小节我们知道,CentralCache层存储的对象是一个一个的哈希桶,而哈希桶内存储的对象是一个类型为Span的双向链表,所以我们也得设计一个类似于ThreadCache的单向链表一样的变量存储这个双向链表,而且这个单向链表的类型还需要具备有进栈,出栈,插入和删除的功能。对比ThreadCache层,我们设计CentralCache层还需要考虑多线程情况下的死锁问题,根据文章《C++高并发内存池实战:初步了解》可知,在设计CentralCache层时我们还需要对桶进行锁的定义,所以CentralCache层的设计如下:

cpp
class SpanList{public:	SpanList(){    //初始化单向链表		_head = new Span;		_head->_next = _head;		_head->_prev = _head;	}	void PushFront(Span* span){    //进行入栈操作		Insert(Begin(), span);	}	Span* PopFront(){    //进行出栈操作		Span* front = _head->_next;		Erase(front);		return front;	}	void Insert(Span* pos, Span* newSpan){    //进行插入操作		assert(pos);		assert(newSpan); 		Span* prev = pos->_prev;		prev->_next = newSpan;		newSpan->_prev = prev;		newSpan->_next = pos;		pos->_prev = newSpan;	}	void Erase(Span* pos){    //进行删除操作		assert(pos);		assert(pos != _head); 		Span* prev = pos->_prev;		Span* next = pos->_next;		prev->_next = next;		next->_prev = prev;	}private:	Span* _head;    //指向双向链表的头节点public:	std::mutex _mtx;//桶锁};

为了更直观的理解SpanList,我将对SpanList进行画图分析,详细参考下图:

图2.SpanList结构详解

在设计完Span类型还有SPalList单向链表后,我们在CentralCache层的功能还没有实现,那就是返回内存给ThreadCache层的接口函数,还有就是当CentralCache层不存在多余的内存时,我们应该通过一个接口来向PageCache层进行内存的申请。而且CentralCache层不像ThreadCache层一样,针对每一个线程都存在一个ThreadCache,而CentralCache层只存在一个,所以我们需要使用单例的设计模式,针对上述讲到的要求,我们设计了CentralCache层的结构详见下述代码:

cpp
class CentralCache{public:	static CentralCache* GetInstance(){    //返回唯一的CentralCache对象		return &_sInst;	}    //向PageCache层申请内存    Span* GetOneSpan(SpanList& list,size_t size)    //返回内存至ThreadCache层的接口函数	size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);    //管理ThreadCache层释放的内存    void ReleaseListToSpans(void* start, size_t size) private:	SpanList _spanLists[NFREELIST];    //哈希桶 private:	//单例模式(饿汉模式)	CentralCache(){}    //禁止使用构造函数	CentralCache(CentralCache&) = delete;    //禁止使用拷贝构造函数	static CentralCache _sInst;    //定义全局的CentralCache单例对象};

代码中为什么CentralCache类型的构造函数设为私有,且没有声明delete,还是注释为禁用构造函数?这是因为将CentralCache类型的构造函数设为私有,外部无法调用,即不能初始化CentralCache类型的对象。为了方便直观的理解CentralCache层的结构,可参考下图:

图3.CentralCache层结构图

在了解CentralCache层的整体构造后,我们需要对其上述代码中的FetchRangeObj()函数和GetOneSpan()函数进行实现,详见下述代码:

cpp
//CentralCache层获取一定数量的内存返回给ThreadCache层size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size){	size_t index = SizeClass::Index(size);    //根据请求的内存块大小size来确定哈希桶的索引	_spanLists[index]._mtx.lock();    //对调用的哈希桶进行加锁,防止死锁 	Span* span = GetOneSpan(_spanLists[index], size);    //从哈希桶中获取一个Span对象	assert(span);	assert(span->_freeList); 	//从span中获取batchNum个对象	start = span->_freeList;	end = start; 	int i = 0;	int actulNum = 1;	while (i < batchNum - 1 && NextObj(end) != nullptr){    //从双向链表中获取多个内存块		end = NextObj(end);		++i;		++actulNum;	}	span->_freeList = NextObj(end);    //获取下一节点的地址	NextObj(end) = nullptr;	span->_useCount += actulNum;	_spanLists[index]._mtx.unlock();    //对分配的哈希桶进行解锁	return actulNum;}//哈希桶中获取一个Span对象Span* CentralCache::GetOneSpan(SpanList& list,size_t size){	Span* begin = list.Begin();	while (begin != list.End()){		if (begin->_freeList != nullptr){			return begin;		}		else{			begin = begin->_next;		}	} 	//解锁,如果其他线程释放内存就不会阻塞	list._mtx.unlock(); 	//假如CentralCache层没有多余的内存需要去PageCache层中申请资源	PageCache::GetInstance()->_pageMtx.lock();    //解锁	Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));	span->_isUse = true;    //修改Span的状态为以分配	PageCache::GetInstance()->_pageMtx.unlock();    //对PageCache上锁		char* start = (char*)(span->_pageId << PAGE_SHIFT);  //获取分配的span的起始地址	size_t bytes = span->_n << PAGE_SHIFT;				 //获取搭配的span大小	char* end = start + bytes; 	//把span切分成对应小块内存尾插进_freeList,再插入到哈希桶中	span->_freeList = start;	start += size;	void* tail = span->_freeList;	while (start < end){		NextObj(tail) = start;		tail = NextObj(tail);		start += size;	}	NextObj(tail) = nullptr;	list._mtx.lock();    //把span挂到桶里面去的时候加锁	list.PushFront(span); 	return span;}

在实现CentralCache层分配内存后,我们还需要接受上一层所释放的内存。由于PageCache分配的内存都是连续的以页为单位的内存,而这些页在CentralCache层时会被切成一小块,在ThreadCache释放内存时,我们也应该按照切成小块的顺序进行释放,如果乱序的化会导致原本已经可以返回给PageCache层的内存,由于没有拼接为一页导致不能即时返还。所以在返回地址时,我们还需要对ThreadCache释放的内存按序来释放,确保不会产生过多的碎片空间,实现代码如下:

cpp
//管理ThreadCache层释放的内存void CentralCache::ReleaseListToSpans(void* start, size_t size){	size_t index = SizeClass::Index(size);    //获取释放内存对于的哈希桶	_spanLists[index]._mtx.lock();            //对指定的哈希桶进行加锁 	while (start){		void* next = NextObj(start);    //获取下一个节点的指针         //返回释放内存所对应的在PageCache层的地址		Span* span = PageCache::GetInstance()->MapObjectToSpan(start);		NextObj(start) = span->_freeList;    //获取当前Span对象管理的内存链表		span->_freeList = start;		span->_useCount--;		if (span->_useCount == 0){    //当所有的Span对象都空闲时			_spanLists[index].Erase(span);			span->_freeList = nullptr;			span->_next = nullptr;			span->_prev = nullptr; 			_spanLists[index]._mtx.unlock();    //解锁,以便其他线程可申请和释放资源 			PageCache::GetInstance()->_pageMtx.lock();			PageCache::GetInstance()->ReleaseSpanToPageCache(span);//归还内存至PageCache			PageCache::GetInstance()->_pageMtx.unlock(); 			_spanLists[index]._mtx.lock();    //加锁		}		start = next;	} 	_spanLists[index]._mtx.unlock();}

PS:在CentralCache层进行内存的分配时需要对哈希桶进行加锁,当CentralCache层返回内存至PageCache层时则需要把对应的锁释放。在ThreadCache层对内存进行申请和释放时同样需要的对CentralCahce层的哈希桶进行加锁

24次原文点赞;这里的喜欢仅保存在本机
WildPointer

专注系统编程、工程实践与底层技术,记录 C++、Qt、OpenCV 与 VTK 的学习和实践。