C++——高并发内存池(5)--ThreadCache、CentralCache、PageCache的释放内存代码及释放逻辑

文章详细介绍了内存管理系统中的ThreadCache如何释放内存,当链表过长时如何回收到CentralCache。CentralCache通过映射关系找到对应的Span,释放内存。PageCache负责内存的分配和合并,通过_newSpan和_ReleaseSpanToPageCache实现内存块的高效管理。此外,文章提到了Span的_isUse标志用于判断是否可合并。
摘要由CSDN通过智能技术生成

1. thread cache释放内存

class ThreadCache
{
public:

	// 释放内存对象
	void Deallocate(void* ptr, size_t size);

	// 释放对象时,链表过长时,回收内存回到中心缓存
	void ListTooLong(FreeList& list, size_t size);
private:
	FreeList _freeLists[NFREELISTS];
};
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;

1.1 void ThreadCache::Deallocate()

释放内存,首先需要根据对齐的字节数,去寻找在哪个桶内,将释放的内存重新挂在桶上。其次,需要判断一下现在挂在桶上的内存块是不是过于多,过于多时需要还给central cache,即还给span。

/释放空间需要告诉我 你属于哪个对齐数 然后挂到对应的哈希桶上 所以需要传入size
void ThreadCache::Deallocate(void* ptr, size_t size)
{
	assert(ptr);
	assert(size <= MAX_BYTES);

	//找对映射的自由链表桶,将对象插入
	size_t index = SizeClass::Index(size);
	_freeLists[index].Push(ptr);

	//当链表长度大于一次批量申请的内存时,就开始还一段list给central cache
	if (_freeLists[index].Size() >= _freeLists[index].Maxsize())
	{
		ListTooLong(_freeLists[index], size);
	}
}

 1.2 void ThreadCache::ListTooLong()

桶上挂的内存块确实过于长时,需要先将一部分(桶的MaxSize()个数个)从桶结构中pop出来,所以在FreeList类里面新增一个接口,用于删除确定个数个的节点。

1.2.1 void FreeList::PopRange()

class FreeList
{
public:
	void PushRange(void* start, void* end,size_t n)
	{
		NextObj(end) = _freeList;
		_freeList = start;
		_size += n;
	}

	//start  和 end为输出型参数
	void PopRange(void*& start, void*& end, size_t n)
	{
		assert(n<=_size);
		start = _freeList;
		end = start;
		for (size_t i = 0; i < n - 1; i++)
		{
			end = NextObj(end);
		}
		_freeList = NextObj(end);
        NextObj(end) = nullptr;
		_size -= n;
	}

	size_t Size()
	{
		return _size;
	}

	size_t& Maxsize()
	{
		return _maxSize;
	}
private:
	void* _freeList=nullptr;
	size_t _maxSize = 1;//用于辅助判断申请个数
	size_t _size = 0;
};

1.2.2 函数实现

void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
	void* start = nullptr;
	void* end = nullptr;
	list.PopRange(start, end, list.Maxsize());
	//将一个一个切好的小内存 放到所属页的span里面
	CentralCache::GetInstance()->ReleaseListToSpans(start, size);
}

2.central cache 回收内存

class CentralCache
{
public:
	static CentralCache* GetInstance()
	{
		return &_sInst;
	}

	//将一定数量的对象释放到span跨度
	void ReleaseListToSpans(void* start, size_t byte_size);

private:
	static CentralCache _sInst;
	SpanList _spanLists[NFREELISTS]; 
};

 可以看到thread cache还回来的内存仅有开始位置的地址。有开始位置的地址,就可以知道他属于 哪个页,即_pageId,但是我怎么知道_pageId是属于哪一个span的呢?由于span的共有成员中有起始页的_pageId和页数,按道理来说遍历一遍spanLists 也可以解决问题,但是这样效率太慢。

所以需要在分割Span时,就采用哈希表建立_pageId和Span的映射图。

当程序刚启动时,要想分配内存,首先需要pagecache调用NewSpan()函数,所以在该函数内我们需要添加_pageId和span的映射关系。

所以我们在page cache中新增一个私有成员,_idSpanMap 以及一个接口

Span* MapObjectToSpan(void* obj); 用于获取映射关系。

 2.1 储备工作

2.1.1 _idSpanMap 和 建立映射关系

可以知道_pageId,但是不知道是哪个Span,如果采用遍历的方式找,效率太低。

class PageCache
{
public:
	Span* NewSpan(size_t k);

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

	// 释放空闲span回到Pagecache,并合并相邻的span
	void ReleaseSpanToPageCache(Span* span);
private:
    //新增
	std::unordered_map<PAGE_ID, Span*> _idSpanMap;
};

Span* PageCache::NewSpan(size_t k)
{
	assert(k > 0 && k < NPAGES);
	//在page cache中 页号和哈希桶的下标是对应的
	if (!_spanLists[k].Empty())
	{
		Span* kSpan = _spanLists[k].PopFront();
		//记录页号和span的映射 方便给central cache使用
		for (PAGE_ID i = 0; i < kSpan->_n; i++)
		{
			_idSpanMap[kSpan->_pageId + i] = kSpan;
		}
		return kSpan;
	}
	for (size_t i = k + 1; i < NPAGES; i++)
	{
		if (!_spanLists[i].Empty())
		{
			//取走该块大span
			Span* nSpan = _spanLists[i].PopFront();
			//创建一个新的span 即我们需要的页大小的span
			Span* kSpan = new Span;//这里默认值给过了 都是设置好的
			//现在需要将 nSpan的一部分切下来给 kSpan 采用从头切
			kSpan->_pageId = nSpan->_pageId;
			kSpan->_n = k;

			nSpan->_pageId += k;//_pageId是 整个span起始页的编号
			nSpan->_n -= k;

			//我需要的是kSpan 那剩下的nSpan就要挂在对应下标的桶里面
			_spanLists[nSpan->_n].PushFront(nSpan);

            //nSpan是分割剩下的那一部分 整体挂在_spanLists 只映射头尾页号就可以使
            //page Cache回收内存时 进行合并查找 

			_idSpanMap[nSpan->_pageId] = nSpan;
			_idSpanMap[nSpan->_pageId+nSpan->_n -1] = nSpan;

			//kSpan是要被thread cache用的 kSpan会被分割成小内存 
            //且还内存时 只知道指针 从而知道页号
            //所以需要记录每个页号和span的映射
            //方便将回收的小内存块挂对span
			for (PAGE_ID i = 0; i < kSpan->_n; i++)
			{
				_idSpanMap[kSpan->_pageId + i] = kSpan;
			}
			return kSpan;
		}
	}
	//走到这里 说明后面没有大页的span了
	Span* bigSpan = new Span;
	void* ptr = SystemAlloc(NPAGES - 1);
   
    //4G的内存空间 
    //其实就是从0开始 一个一个的页构成的
	bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT; 
 
	bigSpan->_n = NPAGES - 1;
	_spanLists[bigSpan->_n].PushFront(bigSpan);


	//现在要切分bigSpan了 但是按理来说不在项目里面写重复的代码
	//所以采用递归调用
	return NewSpan(k);
}

 2.1.2 有关kSpan 和nSpan映射关系不需要统一的解释

nSpan是分割剩下的那一部分,整体挂在_spanLists,是闲置的。

当PageCache回收内存(后面会讲到)的时候,会对前后相邻页的不同span,进行检查并尝试合并一个更大的span,nSpan是不用的连续的一大段空间,只要头尾被映射就可以拿到这个nSpan尝试合并。


kSpan是要被thread cache用的 kSpan会被分割成小内存,在我们上述实现的释放函数中,小内存的归还内存,只知道指针,而指针最多帮助我们知道页号,_spanLists对应的桶里面那么多span,我们怎么知道还到哪一个span里面呢?

所以需要记录每个页号和span的映射,方便将回收的小内存块挂对span。

2.1.3 Span* PageCache::MapObjectToSpan()


//传入指针 返回该指针在哪个span 这里访问了临界资源区 _idSpanMap
//这个哈希表在使用的时候必须是原子性的 
Span* PageCache::MapObjectToSpan(void* obj)
{
    //地址除以8K 可以得到在哪个页
	PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHIFT);

    std::unique_lock<std::mutex> lock(_pageMtx); //RAII的锁 出了作用域就会释放锁
	auto ret = _idSpanMap.find(id);
	if (ret != _idSpanMap.end())
	{
		return ret->second;
	}
	else
	{
		assert(false);
		return nullptr;
	}
}

2.2 void CentralCache::ReleaseListToSpans()

 确定在_spanLists的哪个下标处,确定span

void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
	//先看看在_spanLists的几号下标处
	size_t index = SizeClass::Index(size);

	_spanLists[index]._mtx.lock();
	while (start)
	{
		void* next = NextObj(start);
		//查看start在哪个span里面
		Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
		NextObj(start) = span->_freeList;
		span->_freeList = start;
		span->_useCount--;
		if (span->_useCount == 0)
		{
			//全都还回来了 把当前的span回收给page cache进行前后合并成一个大的span
			_spanLists[index].Erase(span);
			span->_freeList = nullptr;
			span->_next = nullptr;
			span->_prev = nullptr;
			//后面要交给 page cache去进行操作 
			//把桶锁解除掉 以方便其他进程申请或者释放span
			_spanLists[index]._mtx.unlock();

			PageCache::GetInstance()->_pageMtx.lock();
			PageCache::GetInstance()->ReleaseSpanToPageCache(span);
			PageCache::GetInstance()->_pageMtx.unlock();

			_spanLists[index]._mtx.lock();
		}
		start = next;
	}
	_spanLists[index]._mtx.unlock();
}

 3.PageCache 整理合并内存

依赖什么来合并呢?_useCount吗?但是在线程同时运行时,当一个span刚被切分时,他的_useCount就是0,可能会被直接拿来进行合并。

void PageCache::ReleaseSpanToPageCache(Span* span)

不合理,所以在span类里面再新增一个类型bool _isUse,用来看看当前这个span是否是被使用的,初始化默认为false,当span被切分时,修改 _isUse为true,那什么时候再将_isUse置为false呢?

一开始程序内部桶全是空,我们需要通过NewSpan()函数去申请一个128页的span,然后将kSpan(被申请的那部分)的_isUse置成true,而nSpan._isUse依然是默认值,即false。

也就是说在执行当前代码的时候span的_isUse一直是true,当他已经尝试合并前后页后,才置成false

struct Span
{
	PAGE_ID _pageId=0;
	size_t _n=0;
	Span* _next=nullptr;
	Span* _prev=nullptr;
	size_t _useCount=0;
	void* _freeList=nullptr;
	bool _isUse = false;
};

3.1 合并

合并的整体流程

由于thread cache的释放,导致central cache回收其中过多的内存块,在这个过程中,某个span的_useCount减为0,那么这个span的所有分出去的内存小块就全回来了,所以继续把它往上交给PageCache,进行前后页的合并。

合并逻辑

首先在NewSpan()函数中,针对于空闲的页已经设置了映射关系,这时想找前后页,只需要在_idSpanMap中通过他的_pageId,找到前后页所在的span。

针对于向前合并,prevId=span->_pageId-1;通过prevId找到prevSpan,判断prevSpan._isUse是否为false,如果是则开始合并。合并即让span._pageId赋值成prevSpan._pageId,然后_n等变化。最后记得delete掉不用的span

 向后合并同理,不再赘述。

3.2 void PageCache::ReleaseSpanToPageCache()

Span* PageCache::MapObjectToSpan(void* obj)
{
	//地址除以8K 可以得到在哪个页
	PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHIFT);
	auto ret = _idSpanMap.find(id);
	if (ret != _idSpanMap.end())
	{
		return ret->second;
	}
	else
	{
		assert(false);
		return nullptr;
	}
}
void PageCache::ReleaseSpanToPageCache(Span* span)
{
	//对前后相邻页的span 尝试合并一个更大的span 
	// 在NewSpan()函数中 分割的时候已经保存了  页数 和 span的映射关系

	//向前合并
	while (1)
	{
		//通过相邻的页数找 连续的空间 将两个在物理内存上连续的span 合成一个
		PAGE_ID prevId = span->_pageId - 1;
		//先看一下前面的页在不在
		auto ret = _idSpanMap.find(prevId);
		if (ret == _idSpanMap.end())
		{
			//不在 不能向前合并
			break;
		}
		Span* prevSpan = ret->second;
		if (ret->second->_isUse == true)
		{
			//前面的 相邻的 span在被使用 不能合并
			break;
		}
        if (prevSpan->_n + span->_n > NPAGES - 1)
		{
			break;
		}
		span->_pageId = prevSpan->_pageId;
		span->_n += prevSpan->_n;
		_spanLists[prevSpan->_n].Erase(prevSpan);
		delete prevSpan;
	}
	//向后合并
	while (1)
	{
		PAGE_ID nextId = span->_pageId + span->_n;
		auto ret = _idSpanMap.find(nextId);
		if (ret == _idSpanMap.end()) break;
		Span* nextSpan = ret->second;
		if (nextSpan->_isUse == true) break;
        if (nextSpan->_n + span->_n > NPAGES - 1) break;
		span->_n += nextSpan->_n;
		_spanLists[nextSpan->_n].Erase(nextSpan);
		delete nextSpan;
	}
	_spanLists[span->_n].PushFront(span);
	//当一个Span被使用时_isUse就变成了true 也就是说在执行当前代码的时候他的_isUse一直是true
	//当他已经尝试合并前后页后,才置成false
	span->_isUse = false;
	_idSpanMap[span->_pageId] = span;
	_idSpanMap[span->_pageId + span->_n - 1] = span;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1. 创建一个基于对话框的应用程序。并增加如图所示控件;分别为3个进度条控件关联三个进度条类型的变量;并在对话框的初始化函数中,设定进度条的范围;为编辑框关联一个整型的变量;为12个按钮添加消息处理函数; 2. 定义结构体:用做线程函数的参数传递 typedef struct Threadinfo{ CProgressCtrl *progress;//进度条对象 int speed; //进度条速度 int pos; //进度条位置 } thread,*lpthread; 3. 为对话框增加三个句柄,用于标识各个线程; HANDLE hThread1; //线程1线程句柄 HANDLE hThread2; //线程2线程句柄 HANDLE hThread3; //线程3线程句柄 在增加三个结构体类型的变量,用做线程函数的参数传递; HANDLE hThread1; //线程1线程句柄 HANDLE hThread2; //线程2线程句柄 HANDLE hThread3; //线程3线程句柄 4. 新增一个静态的全局变量,用于记录所有线程的状态:static int GlobalVar=10000; 5. 声明并编写线程函数,注意只能有一个参数,且函数的返回值类型也是固定的;函数名可以自定义; DWORD WINAPI ThreadFun(LPVOID pthread);//线程入口函数 6. 在启动按钮的消息处理函数中编写如下代码thread1.progress=&m_progress1;//进度条对象 thread1.speed=100;//速度 thread1.pos=0;//初始位置 hThread1=CreateThread(NULL,0,ThreadFun,&thread1;,0,0);//创建并开始线程 if (!hThread1) { MessageBox("创建线程失败"); } 7. 编写线程函数(一般是一个死循环,或者需要花费时间很长的算法!否者就失去了多线程的意义) DWORD WINAPI ThreadFun(LPVOID pthread) //线程入口函数 { lpthread temp=(lpthread)pthread;//参数强制转换为结构体类型 temp->progress->SetPos(temp->pos); //设置被传递过来的进度条的位置 while(temp->posspeed); /设置速度 temp->pos++; //增加进度 temp->progress->SetPos(temp->pos); //设置进度条的新位置 GlobalVar--; if(temp->pos==20) { temp->pos=0; //进度条满则归0 } } return true; } 8. 在挂起按钮函数中,编写如下代码: if(SuspendThread(hThread1)==0xFFFFFFFF) { MessageBox("挂起失败!进程可能已经死亡或未创建!"); return; } 9. 在执行按钮函数中,编写如下代码: if(ResumeThread(hThread1)==0xFFFFFFFF) { MessageBox("执行失败!进程可能已经死亡或未创建!"); return; } 10. 在停止按钮函数中,编写如下代码: if(TerminateThread(hThread1,0))//前些终止线程 { CloseHandle(hThread1);//销毁线程句柄 } else { MessageBox("终止进程失败!"); } 11. 为应用程序添加WM_TIMER消息,实时更新全局变量的值到编辑框;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值