C++项目高并发内存池_PageCache设计 以及 项目申请内存联调

紧跟CentralCache设计之后

PageCache整体设计

PageCache结构也是哈希桶结构,每个桶的数据都是Span链表。

  • ThreadCache与CentralCache哈希桶的映射规律一致,PageCache哈希桶是根据页数分桶的。Span节点大小为一页的在哈希桶的第一个桶,Span大小为两页的在第二个桶依次类推
  • CentralCache中Span节点被切分成小内存块给ThreadCache使用,而PageCache中的Span不切块。CentralCache向Page Cache 申请内存时关注的是Span的大小,PageCache中Span内部空间不需要切块。需要Span的大小直接对应了PageCache哈希桶的桶位置

注意:
当CentralCache向PageCache申请内存时,PageCache先检查对应位置有没Span,如果没有则向更大页寻找一个Span,如果找到则分裂成两个。

比如:申请的是4页PageCache,4页PageCache后面没有挂Span,则向后面寻找更大的Span,假设在10页Page位置找到一个Span,则将10页PageCache Span分裂为一个4页Page Span和一个6页Page Span。
在这里插入图片描述
CentralCache向PageCache申请空间,CentralCache是被多线程访问的,PageCache也是被多线程访问,所以PageCache也需要加锁。
注意: PageCache不能再使用桶锁,因为PageCache还要负责CentralCache内存回收工作。PageCache申请一页Span时如果Span链表没有就向两页大小Span链表中拿,所以不能设计成桶锁。

流程
开始时Page Cache桶中没有挂任何Span,此时申请2页Span时先向大页的桶中找Span切分。当大页都找不到时会向系统申请一个128页的Span,将这块空间放到PageCache对应的桶上。将128页切分成2页和126页,2页SpanCentralCache,126页挂到126页对应的桶上。此后要是再申请3页的Span,会依次向下找直到找到126页的Span,将Span差分成小页给CentralCache。

同理当CentralCache收回空间时,PageCache会将空间再合并成页用哈希桶管理,
如果CentralCache Span中的use_cout==0代表这个Span已经没有线程在使用了,将这个Span归还到对应PageCache哈希桶对应的位置,PageCache通过页号查看前后相邻页是否空闲,是的话就合并成更大的页,解决内存碎片问题

这里先考虑申请空间的过程

PageCache也是只有一个,设计成单例模式

C++代码

1.公共资源头文件(Common.h)Windows下直接向堆申请空间VirtualAlloc

注意:PageCache如果调用new向堆申请空间会报非法访问空间的错误,这里只能用VirtualAlloc直接向堆申请空间

#pragma once

#include<iostream>
#include<vector>
#include<time.h>
#include<assert.h>
#include<thread>
#include<mutex>
#include<algorithm>

#include<windows.h>

// 直接去堆上按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
	void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
	// linux下brk mmap等
#endif

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

	return ptr;
}

using std::cout; using std::endl;

static const size_t MAX_BYTE = 126 * 1024;//如果线程申请超过126KB不能直接向ThreadCache申请空间

static const size_t NUMLIST = 208;//ThreadCache中哈希桶的个数

static const size_t NPAGE = 129;//PageCache中哈希桶的个数,0号桶空开从1号桶开始

static const size_t PAGESIZE = 13;//定义一页的大小为2^13(8K)

#ifdef _WIN64
typedef unsigned long long PAGE_ID;//64位下的页号
#elif _WIN32
typedef size_t PAGE_ID;
#else 
	//Linux
#endif // _WIN32


//获取自由链表下一个节点
static void*& NextObj(void* obj) {
	return *(void**)obj;
}

//管理切分好内存的自由链表
class FreeList {
public:
	FreeList() :_freeList(nullptr), MaxSize(1) {}
	void Push(void* obj) {
		//头插
		assert(obj);
		NextObj(obj) = _freeList;
		_freeList = obj;
	}

	void* Pop() {
		//头删
		assert(_freeList);
		void* obj = _freeList;
		_freeList = NextObj(obj);
		return obj;
	}

	void PushList(void* begin, void* end) {
		//头插链表
		NextObj(end) = _freeList;
		_freeList = begin;
	}

	bool Empty() { return _freeList == nullptr; }

	size_t& GetMaxSize() { return MaxSize; }
private:
	void* _freeList;
	size_t MaxSize;//记录freeList链表要向中心缓存层申请多少个内存节点
};

//计算ThreadCache中哈希桶的桶个数
class SizeClass {
public:
	static inline size_t RoundUp(size_t size) {
		if (size <= 128) return _RoundUp(size, 8);
		else if (size <= 1024) return _RoundUp(size, 16);
		else if (size <= 8 * 1024) return _RoundUp(size, 128);
		else if (size < 64 * 1024) return _RoundUp(size, 1024);
		else if (size < 256 * 1024) return _RoundUp(size, 8 * 1024);
		else{
			assert(false);
			return -1;
		}
	}
	//计算在哈希桶的那个位置
	static inline size_t Index(size_t size) {
		assert(size <= MAX_BYTE);
		static int Group[4] = { 16,56,56,56 };
		if (size <= 128) return _Index(size, 8);
		else if (size <= 1024) return _Index(size - 128, 16) + Group[0];
		else if (size <= 8 * 1024) return _Index(size - 1024, 128) + Group[1] + Group[0];
		else if (size < 64 * 1024) return _Index(size - 8 * 1024, 1024) + Group[2] + Group[1] + Group[0];
		else if (size < 256 * 1024) return _Index(size - 64 * 1024, 8 * 1024) + Group[3] + Group[2] + Group[1] + Group[0];
		else {
			assert(false);
			return -1;
		}
	}

	//ThreadCache一个Span中链表挂多少个空间节点
	static size_t ForMemory(size_t size) {
		assert(size > 0);
		size_t num = MAX_BYTE / size;
		if (num < 2) {
			num = 2;//如果对象很大,一次少给ThreadCache一些。
		}
		else if (num > 512) {//如果对象很小,一次多给ThreadCache一些
			num = 512;
		}
		return num;
	}

	//计算PageCache向系统申请页的数目
	static size_t NumForPage(size_t size) {
		size_t NumForMemory = ForMemory(size);//计算中心缓存层一个Span最多要多少节点

		size_t Byte = NumForMemory * size;//总共的字节数

		size_t NumPage = (Byte >> PAGESIZE);//计算申请几页
		if (NumPage == 0) {
			NumPage = 1;//至少给一页空间
		}
		return NumPage;
	}
private:
	static inline size_t _RoundUp(size_t size, size_t AlignNum) {
		size_t AligSize = size;
		if (size % AlignNum != 0) {
			AligSize = (size / AlignNum + 1) * AlignNum;
		}
		return AligSize;
	}
	static inline size_t _Index(size_t size, size_t AlignNum) {
		size_t Pos = 0;
		if (size % AlignNum == 0) {
			Pos = size / AlignNum - 1;
		}
		else {
			Pos = size / AlignNum;
		}
		return Pos;
	}
};

//管理以页为单位的大块空间结构
struct Span {
	PAGE_ID _PageID;//记录是第几页
	size_t _Num;//记录Span里面有多少页
	Span* _next;//双向链表
	Span* _prev;

	size_t use_count;//记录分配了多少个对象给ThreadCahce

	void* FreeList;//切好的小块内存空间

	Span() :_PageID(0), _Num(0), _next(nullptr), _prev(nullptr), use_count(0), FreeList(nullptr) {}
};

class SpanList {
private:
	Span* _head;
public:
	std::mutex _mtx;//桶锁
	SpanList() {//带头双向循环链表
		_head = new Span;
		_head->_next = _head;
		_head->_prev = _head;
	}

	void Insert(Span* pos, Span* NewSpan) {
		//pos位置前插入NewSpan
		assert(pos != nullptr && NewSpan != nullptr);
		Span* prev = pos->_prev;//前一个
		prev->_next = NewSpan;
		NewSpan->_prev = prev;
		NewSpan->_next = pos;
		pos->_prev = NewSpan;
	}

	Span* Pop() {
		Span* front = _head->_next;//带头双向循环链表
		Erase(_head->_next);
		return front;
	}

	void Erase(Span* pos) {//删除SpanList上的节点
		assert(pos != _head);
		Span* prev = pos->_prev;
		Span* next = pos->_next;
		prev->_next = next;
		next->_prev = prev;
	}

	Span* begin() { return _head->_next; }
	Span* end() { return _head; }

	bool Empty() { return _head->_next == _head; }
};

2.线程缓存层实现(ThreadCache.h)

#pragma once

#include"Common.h"
#include"CentralCache.h"

class ThreadCache {
private:
	FreeList _FreeList[NUMLIST];
public:
	//申请释放空间
	void* ApplySpace(size_t size) {
		assert(size <= MAX_BYTE);
		size_t AligSize = SizeClass::RoundUp(size);
		//计算桶位置
		size_t index = SizeClass::Index(AligSize);
		if (!_FreeList[index].Empty()) {
			return _FreeList[index].Pop();
		}
		else {
			//thread cache没有内存向central cache要空间
			return RequestFromCentralCache(index, AligSize);
		}
	}
	void ReleaseSpace(void* ptr, size_t size) {
		assert(size <= MAX_BYTE && ptr != nullptr);
		size_t index = SizeClass::Index(size);//找到第几个桶
		//将这个空间头插到自由链表上
		_FreeList[index].Push(ptr);
	}

	//向中心缓存申请空间
	void* RequestFromCentralCache(size_t index, size_t size) { 
		//慢开始调节算法
		size_t Num = min(SizeClass::ForMemory(size),_FreeList[index].GetMaxSize());//计算中心层给线程缓存多少个空间节点;
		if (Num == _FreeList[index].GetMaxSize()) {
			_FreeList[index].GetMaxSize() += 2;//慢增长,每次申请下次会多给ThreadCache空间
		}
		//依次递增SizeClass::ForMemory(size)是申请上限,一次申请数量不会比它还大
		void* begin = nullptr; void* end = nullptr;
		size_t actualNum = CentralCache::GetCentralCache()->GiveThreadCache(begin, end, Num, size);
		assert(actualNum >= 1);//至少会申请到一个空间节点。
		if (actualNum == 1) {//实际就获得了一个节点,直接将这个节点返回
			return begin;
		}
		else {
			//获得了多个节点,要把这些节点都插入到ThreadCache的哈希桶上
			_FreeList[index].PushList(NextObj(begin), end);//将链表的下一个节点插入桶中,头节点返回给ThreadCache
			return begin;
		}
	}
};

//每个线程都会拥有自己的ThreadCache
static __declspec(thread) ThreadCache* tls_threadcache = nullptr;//用static修饰只在当前文件可见

线程申请独立申请ThreadCache(ConcurrentAlloc.h)

#pragma once

#include"Common.h"
#include"ThreadCache.h"

//线程调用申请ThreadCache空间
static void* ConcurrentAlloc(size_t size) {
	//获取线程自己的ThreadCache
	if (tls_threadcache == nullptr) {
		tls_threadcache = new ThreadCache;
	}
	cout << std::this_thread::get_id()<<" "<<tls_threadcache<< endl;
	return tls_threadcache->ApplySpace(size);
}

static void ConcurrentFree(void* ptr, size_t size) {
	//释放时每个线程一定有tls_threadcache
	assert(tls_threadcache != nullptr);
	tls_threadcache->ReleaseSpace(ptr,size);
}

3.中心缓存层实现(CentralCache.h / CentralCache.cpp)

#pragma once

#include"Common.h"

//中心缓存只有一个,每个线程都可以看见,所以中心缓存类采用单例模式(饿汉模式)
class CentralCache {
private:
	SpanList _SpanList[NUMLIST];//与ThreadCache桶的大小相同
	CentralCache(const CentralCache&) = delete;
	static CentralCache _sInst;
public:
	CentralCache() {}
	static CentralCache* GetCentralCache() {
		return &_sInst;
	}

	//获取一个非空Span,需要向PageCache申请空间
	Span* GetSpan(SpanList& List, size_t size);

	size_t GiveThreadCache(void*& start, void*& end, size_t Num, size_t Size);
};
#include"CentralCache.h"
#include"PageCache.h"

CentralCache CentralCache::_sInst;//定义了一个对象

size_t CentralCache::GiveThreadCache(void*& start, void*& end, size_t Num, size_t size) {
	size_t index = SizeClass::Index(size);
	//要求num个内存节点,需要计Span中FreeList空节点个数
	_SpanList[index]._mtx.lock();//加锁

	Span* span = GetSpan(_SpanList[index], size);//解锁在这个函数中
	assert(span != nullptr && span->FreeList != nullptr);

	start = span->FreeList; end = start;
	//变量Num次,找Num个节点
	size_t actualNum = 1;//实际获得几个内存节点
	for (int i = 0; i < Num - 1; i++) {
		if (NextObj(end) == nullptr) break;//如果走到span中FreeList的空,说明span中内存不够Num个,这个时候有多少返回多少
		actualNum += 1;
		end = NextObj(end);
	}
	span->FreeList = NextObj(end);
	//为分出去的内存节点链表添加nullptr
	NextObj(end) = nullptr;
	span->use_count += actualNum;
	_SpanList[index]._mtx.unlock();
	return actualNum;
}

Span* CentralCache::GetSpan(SpanList& List, size_t size) {
	//在哈希桶对应位置Span链表中找是否有Span,没有就向PageCache申请空间

	//遍历桶的Span链表
	Span* it = List.begin();
	while (it != List.end()) {
		if (it->FreeList != nullptr) {
			return it;//这个Span有空间
		}
		else {
			//Span没有空间,继续找下一个链表Span
			it = it->_next;
		}
	}
	//先把CentralCache的桶锁解开,如果其他线程释放内存不会阻塞
	List._mtx.unlock();
	//没有空闲的Span只能找PageCache,需要加锁,PageCache只能由一个线程访问
	//size是单个对象的大小
	PageCache::GetInst()->_PageMtx.lock();
	Span* span=PageCache::GetInst()->NewSpan(SizeClass::NumForPage(size));
	PageCache::GetInst()->_PageMtx.unlock();

	//获得了一块大Span,这块Span这时被线程单独看到,不需要加锁(没有挂到桶上)
	//Span起始地址
	char* start = (char*)((span->_PageID) << PAGESIZE);
	size_t ByteSize = (span->_Num) << PAGESIZE;
	char* end = start + ByteSize;
	//把Span内部大块内存切成自由链表链接起来
	span->FreeList = start;
	start += size;//自由链表的头节点
	void* tail = span->FreeList;
	while (start < end) {
		NextObj(tail) = start;
		tail = NextObj(tail);
		start += size;
	}
	List._mtx.lock();
	List.Insert(List.begin(), span);//将Span挂到桶上,此时需要加桶锁
	return span;
}

4.页缓存实现过程(PageCache.h / PageCache.cpp)

#pragma once

#include"Common.h"

class PageCache {
private:
	SpanList _SpanList[NPAGE];
	static PageCache _sInst;

	PageCache() {}
public:
	std::mutex _PageMtx;

	PageCache(const PageCache&) = delete;

	static PageCache* GetInst() {
		return &_sInst;
	}

	//获取NumPage页的Span
	Span* NewSpan(size_t NumPage);
};
#include"PageCache.h"

PageCache PageCache::_sInst;

Span* PageCache::NewSpan(size_t NumPage) {//NumPage是页数
	assert(NumPage > 0 && NumPage < NPAGE);

	//看当前位置桶中是否有Span
	if (!_SpanList[NumPage].Empty()) {
		return _SpanList[NumPage].Pop();
	}
	//对应位置的桶是空,检查后面桶里有没有Span,将大Span切分成小Span
	for (size_t i = NumPage + 1; i < NPAGE; i++) {
		if (!_SpanList[i].Empty()) {//有一个桶存在,切分大Span成NumPage页Span和N-NumPage页的Span
			Span* NumPageSpan = new Span;
			Span* NSpan = _SpanList[i].Pop();
			//头切
			NumPageSpan->_PageID = NSpan->_PageID;
			NumPageSpan->_Num = NumPage;
			NSpan->_PageID += NumPage;
			NSpan->_Num -= NumPage;

			//将切下的Span挂到其他桶上
			_SpanList[NSpan->_Num].Insert(_SpanList[NSpan->_Num].begin(), NSpan);
			return NumPageSpan;
		}
	}
	//所有桶都没有Span,直接向堆中申请一大块空间,将这块空间挂到最后一个桶上
	Span* BigSpan = new Span;
	void* ptr = SystemAlloc(NPAGE - 1);
	BigSpan->_PageID = (PAGE_ID)ptr >> PAGESIZE;
	BigSpan->_Num = NPAGE - 1;
	_SpanList[BigSpan->_Num].Insert(_SpanList[BigSpan->_Num].begin(), BigSpan);
	//在调用自己向Central Cache发送空间
	return NewSpan(NumPage);
}

申请内存测试

#include"ConcurrentAlloc.h"

void TestConcurrentAlloc() {
	void* p1 = ConcurrentAlloc(6);
	void* p2 = ConcurrentAlloc(8);
	void* p3 = ConcurrentAlloc(1);
	void* p4 = ConcurrentAlloc(7);
	void* p5 = ConcurrentAlloc(18);

	cout << p1 << endl;
	cout << p2 << endl;
	cout << p3 << endl;
	cout << p4 << endl;
	cout << p5 << endl;
}
int main()
{
	TestConcurrentAlloc();
	return 0;
}

在这里插入图片描述

PageCache第二次申请128页空间测试

#include"ConcurrentAlloc.h"

void TestConcurrentAlloc() {
	void* p1 = ConcurrentAlloc(6);
	void* p2 = ConcurrentAlloc(8);
	void* p3 = ConcurrentAlloc(1);
	void* p4 = ConcurrentAlloc(7);
	void* p5 = ConcurrentAlloc(18);

	cout << p1 << endl;
	cout << p2 << endl;
	cout << p3 << endl;
	cout << p4 << endl;
	cout << p5 << endl;
}

void TestConcurrentAlloc2() {
	for (size_t i = 0; i < 1024; i++) {
		void* p1 = ConcurrentAlloc(6);
		cout << p1 << endl;
	}
	//第二次申请128页
	void* p2 = ConcurrentAlloc(6);
	cout << p2 << endl;
}

int main()
{
	//TestConcurrentAlloc();
	TestConcurrentAlloc2();
	return 0;
}

在这里插入图片描述
项目走到这里申请空间过程基本没有什么太大的问题了,接下来就是释放空间的过程

代码位置

Github
Gitee

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NUC_Dodamce

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值