高并发内存池(五):ThreadCache、CentralCache和PageCache的内存回收机制 及 释放内存过程的调试

目录

ThreadCache的内存回收机制

补充内容1

补充内容2

补充内容3

新增关键函数ListTooLong

CentralCache的内存回收机制

补充内容1

新增关键函数MapObjectToSpan

新增关键函数ReleaseListToSpans

PageCache的内存回收机制

补充内容1

补充内容2

新增关键函数ReleaseSpanToPageCache

目前文件状态

Common.h

ObjectPool.h

ConcurrentAlloc.h

PageCache.h

CentralCache.h

ThreadCache.h

PageCache.cpp

CentralCache.cpp

ThreadCache.cpp

unitTest.cpp

调试过程

新增函数TestConcurrentAlloc2

内存释放流程图

多线程内存申请和释放测试


ThreadCache的内存回收机制

补充内容1

在FreeList类中中新增PopRange函数,用于删除MaxSize个内存结点

cpp 复制代码
//头删n个内存结点
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;
}

补充内容2

在FreeList类中新增记录当前链表中内存结点个数的变量_size

同时在Pop、Push、PushRange、PopRange函数中增加计数操作

补充内容3

在FetchFromCentralCache函数中调用PushRange函数时新增表示插入结点个数的参数

(actualNum - 1是因为此时有一个已经被申请者使用了,添加是为 了_size的计数)

新增关键函数ListTooLong

cpp 复制代码
//链表过长将切割
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
    //输出型参数
	void* start = nullptr;
	void* end = nullptr;

	//头删指定位置自由链表中MaxSize个内存结点
	list.PopRange(start, end, list.MaxSize());

	//将ThreadCache归还的MaxSize个内存结点挂在CentralCache的某个span上
	CentralCache::GetInstance()->ReleaseListToSpans(start,size);//不需要给end,因为end在经过PopRange后必然指向空
}

CentralCache的内存回收机制

补充内容1

在PageCache类中新增unordered_map类型的成员变量_idSpanMap

并在NewSpan函数切分span时,填写页号和kSpan的映射关系

(kSpan管理下的所有页的页号映射到kSpan)

cpp 复制代码
//建立页号和span间得映射关系
std::unordered_map<size_t, Span*> _idSpanMap;

新增关键函数MapObjectToSpan

cpp 复制代码
//内存结点的地址->页号->span的映射
Span* PageCache::MapObjectToSpan(void* obj)
{
	size_t id = ((size_t)obj >> PAGE_SHIFT);//页号 = 内存结点的地址 / 页大小 
	auto ret = _idSpanMap.find(id);//在_idSpanMap中寻找对应的span地址
	if (ret != _idSpanMap.end())
	{
		return ret->second;//找到了就返回该span的地址
	}
	else
	{
		assert(false);//找不到就报错
		return nullptr;
	}
}

注意事项:我们无法直接通过从Thread Cache中归还的内存结点的地址确定该内存结点要被归还给CentralCache中的哪个span,需要先将该内存结点的地址转换为页号(内存结点都是从页中分配的)*,再通过页号和span的映射关系确定要归还的span是哪个*

新增关键函数ReleaseListToSpans

cpp 复制代码
//将从ThreadCache获得多个内存结点整合为一个span
void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
	size_t index = SizeClass::Index(size);
	_spanLists[index]._mtx.lock();

    //start指向的是一串内存结点的头结点
	while (start)
	{
		void* next = NextObj(start);
		Span* span = PageCache::GetInstance()->MapObjectToSpan(start);//确定要归还的span

        //头插内存结点到span中
		NextObj(start) = span->_freelist;
		span->_freelist = start;
        
		span->_useCount--;//每插入一个该span的_useCount--,表示有一个分配出去的小块内存回来
		
        //当前span的_useCount为0表示当前span切分出去的所有小块都回来了
		//此时可以将该span传递给PageCache尝试做前后页的合并,得到一个管理更多页的span
		if (span->_useCount == 0)
		{
			_spanLists[index].Erase(span);//将当前的span从CentralCache的某个位置上取下

			//参与到PageCache中进行合并的span不需要自由链表等内容,置空即可
			span->_freelist = nullptr;
			span->_next = nullptr;
			span->_prev = nullptr;  

			_spanLists[index]._mtx.unlock();//不用了就解锁,避免CentralCache中的锁竞争

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

			_spanLists[index]._mtx.lock();//合并完后还要再上锁,为了让当前线程走完ReleaseListToSpans函数		
        }

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

注意事项:

1、归还的内存结点都是同一个span下的只不过可能隶属于不同的页,所以只需要将start指向的结点的地址传递给MapObjectToSpan函数,就可以将这一串的内存结点归还给它们所属的span了

2、加解锁时要考虑好多线程的锁竞争问题

PageCache的内存回收机制

补充内容1

在Span中新增变量_isUse,用于标记当前的span的所处位置

(为true表示在CentralCache中,为false表示为PageCache中)

在PageCache为CentralCache分配span时将_isUse设为true

CentralCache归还的span在PageCache中挂起后将_isUse设为false

cpp 复制代码
struct Span
{
    ...
    bool _isUse = false;//初始时设为false,因为所有span都是从PageCache中得到的
}

补充内容2

在NewSpan中新增nSpan的首尾页号与nSpan的映射关系

(此时所有在PageCache中出现过的span都应该能通过页号找到)

新增关键函数ReleaseSpanToPageCache

停止合并原则:

  • 前/后页不存在:通过页号查找不到span,表示该span未在PageCache中出现过
  • 前/后页的span被占用:查找的span在PageCache中出现过,但此时被分给了CentralCache
  • 合并后的总页数大于128:查找的span在PageCache中,但和当前span合并后页数大于128
cpp 复制代码
//将CentralCache归还的span与PageCache中的空闲span进行合并
void PageCache::ReleaseSpanToPageCache(Span* span)
{
	//向前合并
	while (1)
	{
		size_t prevId = span->_PageId - 1;//获取前页的页号
		auto ret = _idSpanMap.find(prevId);//由页号确定在哈希表中的位置

		//通过页号查找不到span,表示该span未在PageCache中出现过,不合并
		if (ret == _idSpanMap.end())
		{
			break;
		}

		//查找的span在PageCache中出现过,但此时被分给了CentralCache,不合并
		Span* prevSpan = ret->second;
		if (prevSpan->_isUse == true)
		{
			break;
		}

		//查找的span在PageCache中,但和当前span合并后页数大于128,不合并
		if (prevSpan->_n + span->_n > NPAGES - 1)
		{
			break;
		}

		_spanLists[prevSpan->_n].Erase(prevSpan);//将PageCache中prevSpan->_n桶处的span进行删除

		span->_PageId = prevSpan->_PageId;
		span->_n += prevSpan->_n;
		

		delete prevSpan;//因为NewSpan中是new了一个bigSpan的prevSpan是bigSpan的一部分
	}

	//向后合并
	while (1)
	{
		size_t nextId = span->_PageId + span->_n + 1;//当前span管理的页的后一个span的首页页号
		auto ret = _idSpanMap.find(nextId);//获取页号对应的桶位置

		//通过页号查找不到span,表示该span未在PageCache中出现过,不合并
		if (ret == _idSpanMap.end())
		{
			break;
		}

		//查找的span在PageCache中出现过,但此时被分给了CentralCache,不合并
		Span* nextSpan = ret->second;
		if (nextSpan->_isUse == true)
		{
			break;
		}

		//查找的span在PageCache中,但和当前span合并后页数大于128,不合并
		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在PageCache中挂起
	span->_isUse = false;//将当前span的_isUse设为false,便于后续线程的span对本span的合并

	//重新存放首尾页的映射关系
	_idSpanMap[span->_PageId] = span;
	_idSpanMap[span->_PageId + span->_n - 1] = span;
}

目前文件状态

Common.h

cpp 复制代码
#pragma once
#include <iostream>
#include <vector>
#include <algorithm>
#include <thread>
#include <unordered_map>
#include <time.h>
#include <assert.h>
#include <windows.h>
#include <mutex>
using std::cout;
using std::endl;

//static const 和 const static 的效果是相同的,static const(或者 const static)的作用是将修饰的变量限定为只读且仅限于当前文件

static const size_t MAX_BYTES = 256 * 1024;//规定ThreadCache最大申请内存为MAX_BYTES
static const size_t NFREELIST = 208;	   //规定ThreadCache和CentralCache中均只有208个桶
static const size_t NPAGES = 129;		   //规定PageCache中span存放的最大页数为129(为了直接映址法)
static const size_t PAGE_SHIFT = 13;       //规定一个页的大小为8KB,即2的13次方


//Windows环境下通过封装Windows提供的VirtualAlloc函数,从而达到不使用malloc函数,直接向操作系统申请以页为单位的内存
inline static void* SystemAlloc(size_t kpage)//kpage表示页数
{
#ifdef _WIN32
	void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);//包含<windows.h>文件才有它们三个的定义以及使用VirtualAlloc函数
#endif
	if (ptr == nullptr)
		throw std::bad_alloc();//抛异常
	return ptr;
}

//获取下一个结点的地址
//static限制NextObj的作用域防止其它文件使用extern使用NextObj函数,传引用返回避免防止拷贝
static void*& NextObj(void* obj)
{
	return *(void**)obj;
}

//管理小块内存的自由链表
class FreeList
{
public:
	//头插
	void Push(void* obj)
	{
		assert(obj);
		NextObj(obj) = _freeList;
		_freeList = obj;

		++_size;
	}

	//一次性插入n个结点
	void PushRange(void* start, void* end,size_t n)
	{
		NextObj(end) = _freeList;
		_freeList = start;

		_size += n;
	}

	//头删
	void* Pop()
	{
		assert(_freeList);//当前负责释放内存结点的自由链表不能为空
		void* obj = _freeList;
		_freeList = NextObj(obj);

		--_size;
		return obj;
	}

	//头删n个内存结点
	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;
	}

	//判空,当前自由链表是否为空
	bool Empty()
	{
		return  _freeList == nullptr;
	}

	//当前链表中可以存在的最大结点个数,与慢调节算法配合使用
	size_t& MaxSize()
	{
		return _maxSize;
	}

	//返回当前链表的结点个数
	size_t& Size()
	{
		return _size;
	}

private:
	void* _freeList = nullptr;
	size_t _maxSize = 1;//链表结点的初始最大个数
	size_t _size = 0;//当前链表中结点的个数
};


//存放三种常用计算函数的类
class SizeClass
{
public:
	//基本原则:申请的内存越大,所需要的对齐数越大
	//整体控制在最多10%左右的内碎片浪费
	//如果要的内存是15byte,那么在1,128范围内按8byte对齐后的内碎片应该为1,1/16=0.0625四舍五入就是百分之十
	//[1,128]                    按8byte对齐			freelist[0,16)     128 / 8  = 16
	//[128+1.1024]				 按16byte对齐			freelist[16,72)    896 / 16 = 56
	//[1024+1,8*1024]			 按128byte对齐			freelist[72,128)	...
	//[8*1024+1,64*1024]		 按1024byte对齐			freelist[128,184)  ...
	//[64*1024+1,256*1024]		 按8*1024byte对齐		freelist[184,208)	...


	//内联函数在程序的编译期间在使用位置展开,一般是简短且频繁使用的函数,减少函数调用产生的消耗,增加代码执行效率

	static inline size_t _RoundUp(size_t bytes, size_t alignNum)//(申请的内存大小,规定的对齐数)
	{
		size_t alignSize = 0;//对齐后的内存大小
		if (bytes % alignNum != 0)//不能按与之配对的对齐数进行对齐的,就按照与其一起传入的对齐数进行对齐计算
		{
			alignSize = (bytes / alignNum + 1) * alignNum;//bytes = 50 alignNum = 8,对齐后大小就为56
		}
		else//能按与其配对的对齐数进行对齐的,对齐后大小就是传入的申请内存大小
		{
			alignSize = bytes;//bytes = 16 alignNum = 8,对齐后大小就为16
		}
		return alignSize;
	}

	//对齐函数
	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退出即可
			assert(false);
			return -1;
		}
	}

	基础版寻找桶位置(普通人能想到的)
	//static inline size_t Index(size_t bytes, size_t alignnum)//申请内存,对齐数
	//{

	//	if (bytes % alignnum == 0)//刚刚好和对齐数一样
	//	{
	//		return bytes / alignnum - 1;//第一个桶的下标为0,故后续桶计算出的位置要-1
	//	}
	//	else
	//	{
	//		return bytes / alignnum;
	//	}
	//}

	//进阶版寻找桶位置(太巧了):
	static inline size_t _Index(size_t bytes, size_t align_shift)//申请内存,对齐数的次方
	{
		return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;
	}

	static inline size_t Index(size_t bytes)
	{
		assert(bytes <= MAX_BYTES);//确保传入申请内存的最大大小不超过256KB,MAX_BYTES会额外定义
		static int group_array[4] = { 16,56,56,56 };//提前写出计算好的每个链表的个数
		if (bytes <= 128)
		{
			return _Index(bytes, 3);
		}
		else if (bytes <= 1024)
		{
			return _Index(bytes - 128, 4) + group_array[0];//加上上一个范围内的桶的个数
		}
		else if (bytes <= 8 * 1024)
		{
			return _Index(bytes - 1024, 7) + group_array[0] + group_array[1];
		}
		else if (bytes <= 64 * 1024)
		{
			return _Index(bytes - 8 * 1024, 10) + group_array[0] + group_array[1] + group_array[2];
		}
		else if (bytes <= 256 * 1024)
		{
			return _Index(bytes - 64 * 1024, 13) + group_array[0] + group_array[1] + group_array[2] + group_array[3];
		}
		else
		{
			assert(false);
			return -1;
		}
	}

	//理论上最好一次要分配多少内存结点
	static size_t NumMoveSize(size_t size)//size表示要申请的对象的大小
	{
		assert(size > 0);
		if (size == 0)//一般也不会为0
		{
			return 0;
		}

		//num∈[2,512]
		int num = MAX_BYTES / size;
		if (num < 2)//所需内存结点个数过少,就多给,最少要給2个
		{
			//num = 256KB / 512KB = 0.5 ≈ 1 个
			//num越小表示所需内存越大,此时如果只给1个可能下一次再要这么多的时候就还要申请一次,不如直接给两个有备无患
			//所以规定ThreadCache一次向CentralCache中所能申请到的最小内存结点个数为2个
			num = 2;
		}
		if (num > 512)//所需内存结点个数过多,就少给,最多能给512个
		{
			//num = 256KB / 50Byte ≈ 5242个
			//如果要一次性提供太多的内存结点,可能造成CentralCache不够,那么CentralCache还要去向PageCache中申请新的span,麻烦
			//所以规定ThreadCache一次向CentralCache中所能申请到的最大内存结点个数为512个
			num = 512;
		}
		return num;
	}


	//一次性要向堆申请多少个页
	static size_t NumMovePage(size_t size)
	{
		size_t batchnum = NumMoveSize(size);//需要分配的内存结点个数
		size_t npage = (batchnum * size) >> PAGE_SHIFT;//(需要分配的内存结点个数 * 单个内存结点的大小) / 每个页的大小
		
		//位运算相比于/运算速度更快
		//128KB >> 13 = 16页
		//128KB / 8 = 16页

		if (npage == 0)//所需页数为0,就主动给分配一个
			npage = 1;
		return npage;
	}
};


struct Span
{
	size_t _PageId;//当前span管理的连续页的起始页的页号
	size_t _n;//当前span管理的页的数量

	Span* _next = nullptr;//用于链接其它span结点
	Span* _prev = nullptr;

	size_t _useCount = 0;//当前span中切好小块内存,被分配给thread cache的数量
	void* _freelist = nullptr; //当前span下挂的自由链表的头指针

	bool _isUse = false;//初始时设为false,因为所有span都是从PageCache中得到的
};


//管理某个桶下所有span1的数据结构(带头双向循环链表)
class SpanList
{
public:
	//构造初始的SpanList
	SpanList()
	{
		_head = new Span;
		_head->_next = _head;
		_head->_prev = _head;
	}

	//返回指向链表头结点的指针
	Span* Begin()
	{
		return _head->_next;
	}

	//返回指向链表尾结点的指针
	Span* End()
	{
		return _head;
	}

	//头插
	void PushFront(Span* span)
	{
		Insert(Begin(), span);
	}

	//在指定位置插入span
	//位置描述:prev newspan pos
	void Insert(Span* pos, Span* newSpan)
	{
		assert(pos);//指定位置不能为空
		assert(newSpan);//新的span不能为空

		Span* prev = pos->_prev;//存放插入位置的前一个span的位置
		prev->_next = newSpan;
		newSpan->_prev = prev;
		newSpan->_next = pos;
		pos->_prev = newSpan;
	}

	//头删
	Span* PopFront()
	{
		Span* front = _head->_next;//_head->_next指向的是那个有用的第一个结点而不是哨兵位
		Erase(front);
		return front;//删掉后就要用,所以要返回删掉的那块内存的地址	
	}

	//删除指定位置的span(为了还给PageCache)
	//位置描述:prev pos next
	void Erase(Span* pos)
	{
		assert(pos);//指定位置不能为空
		assert(pos != _head);//不能将头指针删除
	
		//暂存一下位置
		Span* prev = pos->_prev;
		Span* next = pos->_next;


		prev->_next = next;
		next->_prev = prev;
	}

	//判断是否为空
	bool Empty()
	{
		return _head->_next == _head;
	}

	std::mutex _mtx; //桶锁
private:
	Span* _head = nullptr;
};

ObjectPool.h

cpp 复制代码
#include "Common.h"

template<class T>//模板参数T
class ObjectPool
{
public:

	//为T类型的对象构造一大块内存空间
	T* New()
	{
		T* obj = nullptr;

		if (_freelist != nullptr)
		{
			//头删
			void* next = *((void**)_freelist);//next指向自由链表的第二个结点
			obj = _freelist;
			_freelist = next;
			return obj;//返回指向从自由链表中分配的结点的指针
		}
		else//自由链表没东西才会去用大块内存
		{
			//剩余内存不够一个T对象大小时,重新开大块空间
			if (_remainBytes < sizeof(T))
			{
				_remainBytes = 128 * 1024;//初始设定_remainBytes为128Kb大小,其实也是设定了每次要重新申请的大块内存的大小为128Kb
				_memory = (char*)SystemAlloc(_remainBytes >> 13);//向SystemAlloc函数传递的是要向操作系统申请的页数而不是整体的字节数(在SystemAlloc函数中会再次转换为具体字节数)
				if (_memory == nullptr)
				{
					throw std::bad_alloc();//申请失败就抛异常
				}
			}

			obj = (T*)_memory;
			size_t objsize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);//无论T对象需要的内存大小有多大,则每次分配的内存应该大于等于当前环境下一个指针的大小,从而保证可以顺利存放下一个结点的地址
			_memory += sizeof(T);
			_remainBytes -= sizeo(T);//每次分配后重新结算剩余字节数
		}

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

	//回收内存
	void Delete(T* obj)//传入指向要回收的对象的指针
	{
		//显示调用析构函数清理对象
		obj->~T();

		/*可以不考虑链表是否为空的情况,直接头插即可,因为_freelist起始为空(不信自行带入测试)
		if(_freelist == nullptr)//链表为空就先头插
		{
			_freelist = obj;
			//*(int*)obj = nullptr;//淘汰
			*(void**)obj = nullptr;
		}
		else//头插
		{
			*(void**)obj = _freelist;
			_freelist = obj;
		}
		*/

		//修改后
		*(void**)obj = _freelist;
		_freelist = obj;
	}

private:
	char* _memory = nullptr;//指向大块内存的指针
	size_t _remainBytes = 0;//大块内存在切分过程中剩余字节数
	void* _freelist = nullptr;//自由链表,因为借用内存的对象的类型是不确定的所以要使用void*
};

ConcurrentAlloc.h

cpp 复制代码
#pragma once
#include "Common.h"
#include "ThreadCache.h"

//线程局部存储TLS:是一种变量的存储方法,这个变量在它所在的线程内是安全可访问的,但是不能被其它线程访问,这样就保持了数据的线程独立性。

//向内存空间申请ThreadCache
static void* ConcurrentAlloc(size_t size)
{
	//通过TLS方法,每个线程可以无锁的获取自己专属的ThreadCache对象
	if (pTLSThreadCache == nullptr)
	{
		pTLSThreadCache = new ThreadCache;
	}

	//打印线程id(便于调试)
	cout << std::this_thread::get_id() << ":" << pTLSThreadCache << endl;

	//Allocate函数执行时可能会经历很多文件比如GenOneSpan,NewSpan等才能返回,正常情况下返回的结果就是申请到的内存的地址
	return pTLSThreadCache->Allocate(size);
}

//释放线程的ThreadCache
static void ConcurrentFree(void* ptr,size_t size)
{
	//理论上释放时pTLSThreadCache不会为空
	assert(pTLSThreadCache);
	pTLSThreadCache->Deallocate(ptr,size);
}

PageCache.h

cpp 复制代码
#pragma once
#include "Common.h"

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

	//从PageCache现存的span申请一个k页的span,或向堆重新申请一个k页的span
	Span* NewSpan(size_t k);
	std::mutex _pageMtx;//用于为PageCache整体上锁
	
	//获取从内存结点到span的映射
	Span* MapObjectToSpan(void* obj);
	 
	//将CentralCache中归还的span合并成一个更大的span
	void ReleaseSpanToPageCache(Span* span);

private:
	SpanList _spanLists[NPAGES];

//建立页号和span地址间得映射关系
std::unordered_map<size_t, Span*> _idSpanMap;


	PageCache() {};
	PageCache(const PageCache&) = delete;

	static PageCache _sInst;
};

CentralCache.h

cpp 复制代码
#pragma once
#include "Common.h"

class CentralCache
{
public:
	//获取实例化好的静态成员对象的地址
	static CentralCache* GetInstance()
	{
		return &_sInst;
	}

	//为ThreadCache分配一定数量的内存结点
	size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);

	//从PageCache获取一个非空的span
	Span* GetOneSpan(SpanList& list, size_t size);


	//将从ThreadCache获得多个内存结点整合为一个span
	void ReleaseListToSpans(void* start, size_t size);

private:
	SpanList _spanLists[NFREELIST];//centralcache的桶数量与threadcache一样

	//单例模式的实现方式是构造函数和拷贝构造函数私有化
	CentralCache()
	{};

	CentralCache(const CentralCache&) = delete;
	//C++11中,当我们定义一个类的成员函数时,如果后面使用"=delete"去修饰,那么就表示这个函数被定义为deleted,也就意味着这个成员函数不能再被调用,否则就会出错

	//使用饿汉模式,保证程序在启动时就会创建对象,避免了线程安全问题
	static CentralCache _sInst;//静态成员变量在编译时就会被分配内存
};

ThreadCache.h

cpp 复制代码
#pragma once
#include "Common.h"

class ThreadCache
{
public:

	void* Allocate(size_t bytes);//(申请的内存大小)
	void Deallocate(void* ptr, size_t size);//(指向待释放内存结点的指针,该内存结点的大小)

	//从中心缓存中获取size大小的对象
	void* FetchFromCentralCache(size_t index, size_t size);//(桶位置,申请的内存的大小)

	//归还内存结点导致自由链表过长时,将超过最大的内存结点数返回给CentralCache
	void ListTooLong(FreeList& list, size_t size);
private:
	FreeList _freeLists[NFREELIST];//构成如图片中所示的ThreadCache有208个桶的基本结构
};

//TLS thread local storage
//static保证该指针只在当前文件可见防止因为多个头文件包含导致的链接时出现多个相同名称的指针
static _declspec(thread)ThreadCache* pTLSThreadCache = nullptr;

PageCache.cpp

cpp 复制代码
#include "PageCache.h"

PageCache PageCache::_sInst;

//int i = 0;//内存申请的测试代码

//从PageCache中获取一个新的非空span
Span* PageCache::NewSpan(size_t k)
{
	assert(k > 0 && k < NPAGES);

	//++i;
	//if (i == 3)
	//{
	//	cout << "获取一个新的span" << endl;
	//}

	//先检查PageCache的第k个桶中有没有span,有就头删
	if (!_spanLists[k].Empty())
	{
		return _spanLists[k].PopFront();
	}

	//检查该桶后面的大桶中是否有span,如果有就进行span分裂
	for (size_t i = k + 1; i < NPAGES; ++i)//因为第一个要询问的肯定是k桶的下一个桶所以i = k + 1
	{
		//后续大桶有span,执行span的分裂
		if (!_spanLists[i].Empty())
		{
			Span* nSpan = _spanLists[i].PopFront();
			Span* kSpan = new Span;

			//在nSpan头部切一个k页的span下来
			kSpan->_PageId = nSpan->_PageId;
			kSpan->_n = k;

			nSpan->_PageId += k;
			nSpan->_n -= k;

			_spanLists[nSpan->_n].PushFront(nSpan);
			//存储nSpan的首尾页号跟nSpan的映射关系,便于PageCache回收内存时的合并查找
			_idSpanMap[nSpan->_PageId] = nSpan;
			_idSpanMap[nSpan->_PageId + nSpan->_n - 1] = nSpan;


		//在span分裂后就建立页号和span得映射关系,便于CentralCahe在回收来自ThreadCache的小块内存时,找到对应的span
		for (size_t i = 0; i < kSpan->_n; ++i)
		{
			_idSpanMap[kSpan->_PageId + i] = kSpan;
		}

			return kSpan;
		}
	}

	//走到这里就说明PageCache中没有合适的span了,此时就去找堆申请一个管理128页的span
	Span* bigSpan = new Span;

	void* ptr = SystemAlloc(NPAGES - 1);//ptr存放堆分配的span的起始地址

	bigSpan->_PageId = (size_t)ptr >> PAGE_SHIFT;//由地址计算页号,页号 = 起始地址 / 页大小,使用位运算更快
	bigSpan->_n = NPAGES - 1;//新的大span中管理的页的数量为128个

	_spanLists[bigSpan->_n].PushFront(bigSpan);
	return NewSpan(k);//重新调用一次自己,那么此时PageCache中就有一个管理k页的span了,可以从PageCache中直接分配了,不需要再考虑该返回什么
	//可以代码复用,递归消耗的资源很小
}

//地址->页号->span的映射
Span* PageCache::MapObjectToSpan(void* obj)
{
	size_t id = ((size_t)obj >> PAGE_SHIFT);
	auto ret = _idSpanMap.find(id);
	if (ret != _idSpanMap.end())
	{
		return ret->second;
	}
	else
	{
		assert(false);
		return nullptr;
	}
}

//将CentralCache归还的span与PageCache中的空闲span进行合并
void PageCache::ReleaseSpanToPageCache(Span* span)
{
	//向前合并
	while (1)
	{
		size_t prevId = span->_PageId - 1;//获取前页的页号
		auto ret = _idSpanMap.find(prevId);//由页号确定在哈希表中的位置

		//通过页号查找不到span,表示该span未在PageCache中出现过,不合并
		if (ret == _idSpanMap.end())
		{
			break;
		}

		//查找的span在PageCache中出现过,但此时被分给了CentralCache,不合并
		Span* prevSpan = ret->second;
		if (prevSpan->_isUse == true)
		{
			break;
		}

		//查找的span在PageCache中,但和当前span合并后页数大于128,不合并
		if (prevSpan->_n + span->_n > NPAGES - 1)
		{
			break;
		}

		_spanLists[prevSpan->_n].Erase(prevSpan);//将PageCache中prevSpan->_n桶处的span进行删除

		span->_PageId = prevSpan->_PageId;
		span->_n += prevSpan->_n;


		delete prevSpan;//因为NewSpan中是new了一个bigSpan的prevSpan是bigSpan的一部分
	}

	//向后合并
	while (1)
	{
		size_t nextId = span->_PageId + span->_n + 1;//当前span管理的页的后一个span的首页页号
		auto ret = _idSpanMap.find(nextId);//获取页号对应的桶位置

		//通过页号查找不到span,表示该span未在PageCache中出现过,不合并
		if (ret == _idSpanMap.end())
		{
			break;
		}

		//查找的span在PageCache中出现过,但此时被分给了CentralCache,不合并
		Span* nextSpan = ret->second;
		if (nextSpan->_isUse == true)
		{
			break;
		}

		//查找的span在PageCache中,但和当前span合并后页数大于128,不合并
		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在PageCache中挂起
	span->_isUse = false;//将当前span的_isUse设为false,便于后续线程的span对本span的合并

	//重新存放首尾页的映射关系
	_idSpanMap[span->_PageId] = span;
	_idSpanMap[span->_PageId + span->_n - 1] = span;
}

CentralCache.cpp

cpp 复制代码
#include "PageCache.h"
#include "CentralCache.h"

//定义
CentralCache CentralCache::_sInst;

//计算实际可为ThreadCache分配的内存结点个数
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
	size_t index = SizeClass::Index(size);
	_spanLists[index]._mtx.lock();//给指定位置的桶上锁

	Span* span = GetOneSpan(_spanLists[index], size);//申请一个非空的span

	//在GetOneSpan函数中,会先判断指定位置的SpanList上是否有自由链表非空的span,
	//如果有则会提供该span,如果没有就去找PageCache要,
	//所以正常情况下GetOneSpan函数肯定能申请到一个符合条件的span 

	assert(span);//申请失败就报错
	assert(span->_freelist);//申请成功但自由链表为空也不行

	//尝试从获取到的span下的自由链表中获取bathcNum个内存结点,若不够就拿实际拥有的数量actualNum
	start = span->_freelist;
	end = start;
	size_t i = 0;//记录循环遍历结点时经过的结点个数
	size_t actualNum = 1;//已经判断过的自由链表不为空,所以肯定有一个,故actualNum的初始值设为1

	//NextObj(end) != nullptr用于防止当前span的freelist中的内存结点个数小于bathcNum,导致越界访问nullptr产生报错
	while (i < batchNum - 1 && NextObj(end) != nullptr)//batchNum-1是因为是数组下标,自行带入数据验证不做过多解释
	{
		end = NextObj(end);
		++i;
		++actualNum;//每获取一个就++
	}

	//缩小当前span下的自由链表中的内存结点
	span->_freelist = NextObj(end);
	NextObj(end) = nullptr;

	span->_useCount += actualNum;//当前span中被分配给ThreadCache的内存结点的个数

	_spanLists[index]._mtx.unlock();//为CentralCache指定位置上的锁解锁,该锁是在GetOneSpan函数最后进行插入时设置的
	return actualNum;//返回自由链表中实际可提供的内存块个数
}


//为指定位置桶下的SpanList申请一个非空的span
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
	//遍历当前位置下的SpanList寻找一个非空的span
	Span* it = list.Begin();
	while (it != list.End())
	{
		if (it->_freelist != nullptr) 
		{
			return it;
		}
		else
		{	
			it = it->_next;
		}
	}

	//此时要先把CentralCache当前位置的桶锁解除,因为在进来之前是加了锁的,防止后续有线程在相同位置无法释放内存
	list._mtx.unlock();

	//上面是用于判断指定位置下的SpanList中是否还有非空的span,有就返回该span的地址没有就向PageCache申请

	PageCache::GetInstance()->_pageMtx.lock();//为PageCache整体上锁
	Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));//从PageCache中获取一个新的非空span
	span->_isUse = true;
	PageCache::GetInstance()->_pageMtx.unlock();//为PageCache整体解锁

	//从PageCache中获取的span是没有进行切分没有自由链表的,需要将其管理的内存进行切分挂在它的freelist下


	//1、计算span管理下的大块内存的起始地址和整个span的大小
	//起始地址 = 页号 * 页的大小,依旧选择位运算
	char* start = (char*)(span->_PageId << PAGE_SHIFT);//选择char*而不是void*便于后续每次+=size的时候是按一字节移动的

	//假设span->_PageId = 5,PAGE_SHIFT = 13,5 >> 13 = 40960(字节)
	//整数值 40960 表示内存中的一个地址位置,通过 (char*) 显示类型转换后,start 就指向了这个内存地址,即span的起始地址

	size_t bytes = span->_n << PAGE_SHIFT;//n表示管理的页的个数,计算该span的大小
	char* end = start + bytes;//end指向span的结束地址

	//2、将start和end指向的大块内存切成多个内存结点,并尾插至自由链表中(采用尾插,使得即使被切割但在物理上仍为连续空间,加快访问速度)
	//①先切下来一块作为头结点,便于尾插
	span->_freelist = start;//PageCache申请下来的span是没有自由链表的所以需要让span的_freelist指向原来start指向的位置
	start += size;
	void* tail = span->_freelist;

	//循环尾插
	while(start < end)
	{
		NextObj(tail) = start;//当前tail指向的内存块的前4/8个字节存放下一个结点的起始地址
		tail = NextObj(tail);
		start += size;
	}

	//向CentralCache中当前的SpanList头插前要上锁,防止其它线程同时方位当前的SpanList
	list._mtx.lock();
	list.PushFront(span);

	return span;//此时该span已经放在了CentralCache的某个桶的SpanList中了,返回该span的地址即可
}

//将从ThreadCache获得多个内存结点整合为一个span
void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
	size_t index = SizeClass::Index(size);
	_spanLists[index]._mtx.lock();

	//start指向的是一串内存结点的头结点
	while (start)
	{
		void* next = NextObj(start);
		Span* span = PageCache::GetInstance()->MapObjectToSpan(start);//确定要归还的span

		//头插内存结点到span中
		NextObj(start) = span->_freelist;
		span->_freelist = start;

		span->_useCount--;//每插入一个该span的_useCount--,表示有一个分配出去的小块内存回来

		//当前span的_useCount为0表示当前span切分出去的所有小块都回来了
		//此时可以将该span传递给PageCache尝试做前后页的合并,得到一个管理更多页的span
		if (span->_useCount == 0)
		{
			_spanLists[index].Erase(span);//将当前的span从CentralCache的某个位置上取下

			//参与到PageCache中进行合并的span不需要自由链表等内容,置空即可
			span->_freelist = nullptr;
			span->_next = nullptr;
			span->_prev = nullptr;

			_spanLists[index]._mtx.unlock();//不用了就解锁,避免CentralCache中的锁竞争

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

			_spanLists[index]._mtx.lock();//合并完后还要再上锁,为了让当前线程走完ReleaseListToSpans函数		
		}

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

ThreadCache.cpp

cpp 复制代码
#include "ThreadCache.h"
#include "CentralCache.h"

//调用ThreadCache中的申请内存对象
void* ThreadCache::Allocate(size_t size)
{
	//范围
	assert(size <= MAX_BYTES);
	size_t allignSize = SizeClass::RoundUp(size);//获取对齐后的大小
	size_t index = SizeClass::Index(size);//确认桶的位置

	if (!_freeLists[index].Empty())
	{
		return _freeLists[index].Pop();//头删符合位置的桶的内存块,表示释放出去一块可以使用的内存
	}
	else
	{
		return FetchFromCentralCache(index, allignSize);//向CentralCache的相同位置处申请内存结点
	}
}


//向CentralCache申请内存结点
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
	//size越小上限越高,最高是512
	size_t batchNum = min(_freeLists[index].MaxSize(), SizeClass::NumMoveSize(size));

	//满调节算法的一部分
	if (_freeLists[index].MaxSize() == batchNum)
	{
		_freeLists[index].MaxSize() += 1;
	}

	//上述部分是满调节算法得到的理论上要分配的结点个数
	//下面新增的内容是计算实际可拿到的结点个数,以及视情况将申请到的未使用的结点重新挂在ThreadCache的自由链表上

	//输出型参数,传入FetchRangeObj函数的是它们的引用,FetchRangeObj函数结束后它们就会分别指向某个自由链表中的两个结点了
	void* start = nullptr;
	void* end = nullptr;

	//理论上要分配的内存结点的数量batchNum和指定位置自由链表中实际拥有的数量内存结点的数量actualNum可能不一致,以实际为主
	size_t actualNum = CentralCache::GetInstance()->FetchRangeObj(start, end, batchNum, size);
	assert(actualNum >= 1);//FetchRangeObj必定会申请到一个自由链表不为空的span,因为该函数中有GetOneSpan函数

	if (actualNum == 1)
	{
		assert(start == end);//此时start和end应该都指向该结点
		return start;//直接返回start指向的结点即可
	}
	else
	{
		_freeLists[index].PushRange(NextObj(start), end,actualNum - 1);//其余分配的结点插入ThreadCache中的指定位置,actualNum - 1是因为此时有一个已经被申请者使用了
		return start;//返回一个立刻要使用的start指向的内存结点
	}
}


//回收当前线程使用完的内存结点
void ThreadCache::Deallocate(void* ptr, size_t size)
{
	assert(ptr);
	assert(size <= MAX_BYTES);

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


	//MaxSize记录的是当前自由链表中结点的的最大个数,若此时自由链表中实际的结点Size个数 >= MaxSize就要一次性归还MaxSize个内存结点给CentralCache中的某个span
	//只归还多出的那一小部分内存结点会导致ThreadCache和CentralCache进行频繁的交互,增加系统调用和锁竞争的次数,从而降低整体性能
	if (_freeLists[index].Size() >= _freeLists[index].MaxSize())
	{
		ListTooLong(_freeLists[index], size);
	}

}

//链表过长将切割
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
	void* start = nullptr;
	void* end = nullptr;

	//头删指定位置自由链表中MaxSize个内存结点
	list.PopRange(start, end, list.MaxSize());

	//将归还的MaxSize个内存结点挂在CentralCache的某个span上
	CentralCache::GetInstance()->ReleaseListToSpans(start,size);//不需要给end,因为end在经过PopRange后必然指向空
}

unitTest.cpp

cpp 复制代码
#include "ObjectPool.h"
#include "ConcurrentAlloc.h"

void Alloc1()
{
	for (size_t i = 0; i < 5; ++i)
	{
		void* ptr = ConcurrentAlloc(6);
	}
}

void Alloc2()
{
	for (size_t i = 0; i < 5; ++i)
	{
		void* ptr = ConcurrentAlloc(7);
	}
}

void TLSTest()
{
	std::thread t1(Alloc1);//创建一个新的线程 t1,并且在这个线程中执行 Alloc1 函数
	std::thread t2(Alloc2);//创建一个新的线程 t2,并且在这个线程中执行 Alloc2 函数

	t1.join();
	t2.join();
}

//申请内存过程的调试
void TestConcurrentAlloc()
{
	//void* p1 = ConcurrentAlloc(6);
	//void* p2 = ConcurrentAlloc(8);
	//void* p3 = ConcurrentAlloc(1);
	//void* p4 = ConcurrentAlloc(7);
	//void* p5 = ConcurrentAlloc(8);

	//cout << p1 << endl;
	//cout << p2 << endl;
	//cout << p3 << endl;
	//cout << p4 << endl;
	//cout << p5 << endl;
	
	//尝试用完一整个span
	for (size_t i = 0; i < 1024; i++)
	{
		void* p1 = ConcurrentAlloc(6);
	}

	//如果用完了一个新的span那么p2指向的地址应该是上一个用完的span的结尾地址
	void* p2 =  ConcurrentAlloc(8);
	cout << p2 <<endl;
}

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

调试过程

新增函数TestConcurrentAlloc2

cpp 复制代码
//释放内存过程的调试(单线程)
void TestConcurrentAlloc2()
{
	//比内存申请时的用例多加两个申请,
    //因为只有这样才能成功使得某个span的_useCount == 0进入PageCache中

	//初始_maxsize = 1
	void* p1 = ConcurrentAlloc(6);//分配一个,_maxsize++ == 2

	void* p2 = ConcurrentAlloc(8);//不够再申请时因为_maxsize == 2,所以分配2个,用一剩一,++_maxsize == 3
	void* p3 = ConcurrentAlloc(1);//用了剩的那个,不用++_maxsize

	void* p4 = ConcurrentAlloc(7);//不够再申请时因为_maxsize == 3,所以分配三个,用一剩二,++_maxsize == 4
	void* p5 = ConcurrentAlloc(8);//用剩余的那两个,不用++_maxsize
	void* p6 = ConcurrentAlloc(6);//用剩余的那两个,不用++_maxsize

	void* p7 = ConcurrentAlloc(8);//不够再申请时因为_maxsize == 4,所以分配4个,用一剩三,++_maxsize == 5
	void* p8 = ConcurrentAlloc(6);//用剩余的三个,不用++_maxsize

	//此时_maxsize == 5,_freeLists[0].size == 2,此时负责分配这10个8字节大小内存结点的span的_useCount == 10,后续调试时可以以此为标准

	//最终代码时不需要传入释放的大小,这里我们先传入
	ConcurrentFree(p1, 6);
	ConcurrentFree(p2, 8);
	ConcurrentFree(p3, 1);
	ConcurrentFree(p4, 7);
	ConcurrentFree(p5, 8);
	ConcurrentFree(p6, 8);
	ConcurrentFree(p7, 8);
	ConcurrentFree(p8, 8);
}

内存释放流程图

~over~

相关推荐
_.Switch24 分钟前
Python 自动化运维持续优化与性能调优
运维·开发语言·python·缓存·自动化·运维开发
徐*红25 分钟前
java 线程池
java·开发语言
尚学教辅学习资料25 分钟前
基于SSM的养老院管理系统+LW示例参考
java·开发语言·java毕设·养老院
1 9 J27 分钟前
Java 上机实践4(类与对象)
java·开发语言·算法
Code apprenticeship28 分钟前
Java面试题(2)
java·开发语言
J不A秃V头A30 分钟前
Python爬虫:获取国家货币编码、货币名称
开发语言·爬虫·python
passer__jw7672 小时前
【LeetCode】【算法】3. 无重复字符的最长子串
算法·leetcode
passer__jw7672 小时前
【LeetCode】【算法】21. 合并两个有序链表
算法·leetcode·链表
sweetheart7-72 小时前
LeetCode22. 括号生成(2024冬季每日一题 2)
算法·深度优先·力扣·dfs·左右括号匹配
SRY122404193 小时前
javaSE面试题
java·开发语言·面试