【高并发内存池】细节处理 + 性能优化 + 总结

在这里插入图片描述

1. 细节处理

1.1 细节1

我们这个项目本来就是要代替系统的内存分配相关函数malloc和free,但是在我们的代码中还是在通过new申请,delete释放。new底层封装了malloc,delete底层封装了free。还是没和malloc和free脱离。

这里我们就可以把项目最开始做的定长内存池拿过来用了。它是一个模板想要什么类型内存找它要就行了。下面就把用到new和delete的地方都换成去找定长内存池申请和释放。

//ObjectPool.h

template<class T>
class ObjectPool
{
   
   
public:
	ObjectPool()
		:_memory(nullptr), _remainBytes(0),_freelist(nullptr)
	{
   
   }

	T* New()
	{
   
   
		T* obj = nullptr;
		//如果链表中有空闲的内存块,就优先使用
		if (_freelist)
		{
   
   
			//头删
			void* next = *(void**)_freelist;
			obj = (T*)_freelist;
			_freelist = next;
		}
		else
		{
   
   
			//if (_memory == nullptr)
			if(_remainBytes < sizeof(T))//如果当前内存块不够一个T类型,就重新申请一大快内存快
			{
   
   
				_remainBytes = 128 * 1024;
				//_memory = (char*)malloc(_remain);
				_memory = (char*)SystemAlloc(_remainBytes >> 13);//按页申请,想要每页8kb,申请多少页
				if (_memory == nullptr)
				{
   
   
					throw std::bad_alloc();
				}
			}
			obj = (T*)_memory;
			//保证内存块至少能放下一个地址
			size_t Size = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
			_memory += Size;
			_remainBytes -= Size;
		}

		// 定位new,显示调用T的构造函数初始化
		new(obj)T;

		return obj;
	}

	void Delete(T* obj)
	{
   
   
		//对释放的内存块进行头插

		// 显示调用析构函数清理对象
		obj->~T();

		//直接头插
		* (void**)obj = _freelist;
		_freelist = obj;
	}

private:
	char* _memory;//指向申请的一大块内存
	size_t _remainBytes;//记录申请一大块内存还剩下多少内存
	void* _freelist;//指向释放后的内存,以链表形式管理起来
};
//PageCache.h

#include"Common.h"
#include"ObjectPool.h"

//所有线程共享一个PageCache
class PageCache
{
   
   
public:
	static PageCache* GetInstance()
	{
   
   
		return &_sInst;
	}

	//从PageCache第k号桶中获取一个span对象
	Span* NewSpan(size_t k);

	// 获取从对象到span的映射
	Span* MapObjectToSpan(void* obj);

	// 释放空闲span回到Pagecache,并合并相邻的span
	void ReleaseSpanToPageCache(Span* span);

private:
	PageCache()
	{
   
   }

	PageCache(const PageCache&) = delete;
	PageCache& operator=(const PageCache&) = delete;

	static PageCache _sInst;
private:
	Spanlist _spanlists[NPAGES];
	std::unordered_map<PAGE_ID, Span*> _idSpanMap;//记录页号和span一一对应关系
	ObjectPool<Span> _spanPool; //申请Span的地方都用定长内存池申请,不用new
public:
	std::mutex _pageMtx; //整个锁
//PageCache.cpp

//从PageCache第k号桶中获取一个span对象
Span* PageCache::NewSpan(size_t k)
{
   
   
	//assert(k > 0 && k < NPAGES);

	assert(k > 0);

	//大于 128page找堆要
	if (k > NPAGES - 1)
	{
   
   
		void* ptr = SystemAlloc(k);
		//申请一个span对象管理这块内存,释放内存的时候要对应的span
		//Span* span = new Span;
		Span* span = _spanPool.New();
		span->_pageid = (PAGE_ID)ptr >> PAGE_SHIFT;
		span->_n = k;
		_idSpanMap[span->_pageid] = span;
		return span;
	}

	//这里加锁递归会有死锁问题,除非用递归互斥锁,还有在外面调用这个函数之前加锁

	// 先检查第k个桶里面有没有span
	if (!_spanlists[k].Empty())
	{
   
   
		Span* Kspan = _spanlists[k].PopFront();


		//将Kspan给CentralCache之前,先将页号和span对应关系记录
		for (size_t i = 0; i < Kspan->_n; ++i)
		{
   
   
			_idSpanMap[Kspan->_pageid + i] = Kspan;
		}

		//将管理k页的span给Central Cache
		return Kspan;
	}

	//走到这里第k个桶没有span,那就继续往下面的桶找
	for (size_t i = k + 1; i < NPAGES; ++i)
	{
   
   
		if (!_spanlists[i].Empty())
		{
   
   
			//将一个管理更多页的span,变成两个span,一个管理k页,一个管理i-k页
			Span* Ispan = _spanlists[i].PopFront();
			//Span* Kspan = new Span;
			Span* Kspan = _spanPool.New();

			Kspan->_pageid = Ispan->_pageid;
			Kspan->_n = k;

			Ispan->_pageid += k;
			Ispan->_n -= k;

			//将Ispan挂在对应大小的桶
			_spanlists[Ispan->_n].PushFront(Ispan);
			//记录挂在PageCache中还未被使用的Ispan的页号和span对应关系
			//这里仅需记录这个span的首页和尾页与Ispan的对应关系即可,
			//不像返回给CentralCache的Kspan的需要把这个Kspan管理的每一页都和Kspan建立映射关系
			//因为合并仅需知道每个Ispan的首页和尾页就可以找到Ispan,而返回给CentralCache的Kspan,
			//首先需要将Kspan切成一块块小内存才行才能再给ThreadCache用,
			//当小内存回来/8kb可能是Kspan管理的其中某一页,才能知道该页对应span
			_idSpanMap[Ispan->_pageid] = Ispan;
			_idSpanMap[Ispan->_pageid + Ispan->_n - 1] = Ispan;


			//将Kspan给CentralCache之前,先将页号和span对应关系记录
			for (size_t i = 0; i < Kspan->_n; ++i)
			{
   
   
				_idSpanMap[Kspan->_pageid + i] = Kspan;
			}

			//将管理k页的span给Central Cache
			return Kspan;
		}
	}

	//走到这里说明,后面的位置都没有大页的span
	//那就去找堆申请一个128page的span
	//Span* bigSpan = new Span;
	Span* bigSpan = _spanPool.New();
	void* ptr = SystemAlloc(NPAGES - 1);
	bigSpan->_pageid = (PAGE_ID)ptr >> PAGE_SHIFT;
	bigSpan->_n = NPAGES - 1;
	//span挂在对应桶下
	_spanlists[bigSpan->_n].PushFront(bigSpan);

	//重复上述过程寻找k号桶的span,一定还回一个span
	return NewSpan(k);
}



// 释放空闲span回到Pagecache,并合并相邻的span
void PageCache::ReleaseSpanToPageCache(Span* span)
{
   
   
	//大于 128page直接还给堆
	if (span->_n > NPAGES - 1)
	{
   
   
		void* ptr = (void*)(span->_pageid << PAGE_SHIFT);
		SystemFree(ptr);
		//delete span;
		_spanPool.Delete(span);
		return;
	}


	// 如何知道相邻的span是否能合并?
	// 通过自己的页号找到相邻的span看是否能合并,如果该页的span在PageCache说明该span还没有被使用,可以合并
	// 如果在CentralCache说明该span正在被使用,不能合并

	// 如何知道一个span是否被使用? 是用span中的usecount是否等于0吗? 不能!!
	// 这里有一个空档时间,当thread1线程通过TLS找到自己threadcache申请内存块,但是没有,
	// 就去找CentralCache,但是CentralCache对应桶下也没有,那就只能去找PageCache了
	// PageCache返回给CentralCache一个span,这个span的usecount初始可是0,
	// CentralCache拿到后对span这一大块内存切成一块块小内存
	// 在挂到对应桶下,但是这时候thread2,要合并这个span,那就有问题了,thread1正准备从这span拿一批量
	// 但是还没有拿到,这个span的usecount可还是0,只有拿走了usecount才会++
	// thread2把这个span和自己span合并了,那就造成线程安全的问题!!

	// 因此需要给span对象加一个isuse成员记录这个span是否被使用


	// 如何通过页号找到相邻的页? 还是得用unordered_map记录页号合span对应关系
	// 但是目前的unordered_map只记录了给CentralCache已经被使用的span的页号和span对应关系
	// 并没有记录在PageCache的span的页号和span对应关系
	// 因此需要把在PageCache的span的页号和span对应关系也要记录在unordered_map中


	//先走前面相邻的span是否能合并,能合并就一直合
	while (1)
	{
   
   
		PAGE_ID prevId = span->_pageid - 1;
		auto ret = _idSpanMap.find(prevId);
		// 前面的页号没有,不合并了(堆申请内存已经到了起始地址)
		if (ret == _idSpanMap.end())
		{
   
   
			break;
		}

		Span* prevSpan = ret->second;
		
		// 前面相邻页的span在使用,不合并了
		if (prevSpan->_isuse == true)
		{
   
   
			break;
		}

		// 合并出超过128页的span没办法挂在桶下,不合并了
		if (prevSpan->_n + span->_n > NPAGES - 1)
		{
   
   
			break;
		}

		// 用span合并prevSpan
		span->_pageid = prevSpan->_pageid;
		span->_n += prevSpan->_n;

		// 将pevSpan从对应的桶中删除
		_spanlists[prevSpan->_n].Erase(prevSpan);
		//delete prevSpan;
		_spanPool.Delete(prevSpan);

		// 这里可能有疑问,那遗留unordered_map中被合并的对应页和prevSpan之间一对一的关系难道不删除吗?
		// 因为prevSpan已经被删除了,在去通过已有页去找span那就是野指针了! 但其实并不用删除.
		// 首先被合并的页已经被span管理起来了,合并结束之后会被挂在对应桶下,并且记录该span首页和尾页与span的对应关系.
		// 当CentralCache要的时候,在把span切分成两个span,返回给CentralCache的Kspan每页都和Kspan重新进行映射
		// 留在PageCache的Ispan的首页和尾页也会和Ispan重新映射
		// 这样的话,以前被合并,遗留下来的页又和新得span建立了映射关系,就不会有通过页找span会有野指针的问题

	}

	//找后面相邻的span合并
	while (1)
	{
   
   
		PAGE_ID nextId = span->_pageid + span->_n;
		auto ret = _idSpanMap.find(nextId);
		// 后面的页号没有,不合并了(堆申请内存已经到了结尾地址)
		if (ret == _idSpanMap.end())
		
评论 129
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值