高并发内存池(四):查缺补漏 与 申请内存过程的调试

目录

查缺补漏

问题:min函数的冲突问题

申请内存过程的调试

当前文件展示

Common.h

ObjectPool.h

ConcurrentAlloc.h

ThreadCache.h

CentralCache.h

PageCache.h

ThreadCache.cpp

CentralCache.cpp

PageCache.cpp

UnitTest.cpp

单进程单span

单进程多span

内存申请流程图


查缺补漏

说明:当我们在经历了高并发内存池的(一)、(二)、(三)三篇文章后,高并发内存池的申请内存的过程已经告一段落了,在正式开始编写释放内存的代码前,我们需要做的是尝试跑一下内存申请的代码,看一看是否有哪里有报错的,并加以解决

问题:min函数的冲突问题

 产生原因:为了使用C++提供的min函数,我们在<Common.h>头文件中包含了C++标准库头文件<algorithm>、它包含了由C++官方提供的各种基本算法,这些算法都在std命名空间下以函数模板的形式存在,在编译阶段才会进行模板实例化,确定T的类型:

namespace std {
    template <class T>
    const T& min(const T& a, const T& b);
    
    template <class T>
    const T& max(const T& a, const T& b);
}

但同时为了使用VirtualAlloc函数所以也包含了Windows API 头文件<windows.h>,在该头文件中min是一个全局的宏:

#define min(a, b)  ((a) < (b) ? (a) : (b))

而宏在预处理阶段会进行替换,函数模板的实例化在编译阶段才会执行,这就导致当执行到编译阶段时,std::min中的min已经被windows中的宏所替换了,无法正常使用

namespace std {
    template <class T>
    const T& ((a) < (b) ? (a) : (b))(const T& a, const T& b);  // 错误
}

解决办法①:取消<windows>中对于min的宏定义

#include <windows.h>
#undef min

申请内存过程的调试

说明:在进行正式编写释放内存的步骤前我们需要先检查一下前面内存申请的代码是否可以正常运行,并通过调试再次深化ThreadCache、CentralCache、PageCache进行申请内存的整个过程

内存结点指的是FreeList中的结点而不是span,span进行切分后才是一个个的内存结点

别搞混了

当前文件展示

基本概念:如果你已经看过之前的函数三篇文章,那么你应该已经了解了很多函数,但是你可能还不知道它们隶属于哪个文件,或者需要包含哪些文件,我来帮你

Common.h

包含内容:

  • 必要的头文件
  • 最大申请内存数、页数、桶个数、右移数等静态常量
  • 对向堆直接申请页的函数的封装函数
  • span、SpanList、Freelist的定义
  • 桶锁
  • 存放计算映射位置,待分配结点个数,对齐数这三种常用计算函数的SizeClass类
#pragma once
#include <iostream>
#include <vector>
#include <algorithm>
#include <thread>
#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;
	}

	//头插多个结点
	void PushRange(void* start, void* end)
	{
		NextObj(end) = _freeList;
		_freeList = start;
	}

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

	//判空
	bool Empty()
	{
		return  _freeList == nullptr;
	}

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

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


//存放三种常用计算函数的类
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下挂的自由链表的头指针
};


//管理某个桶下所有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(要还给page cache)
	//位置描述: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;
};

相关知识点补充:

  • static constconst static 的效果是相同的,static const(或者 const static的作用是将修饰的变量限定为只读且仅限于当前文件
  • static 修饰类中的函数时,会使该函数成为该类的静态成员函数,存储在程序的全局静态存储区(或代码段),而不是为每个对象分配的堆或栈空间,可以直接使用类名调用,不需要也不推荐使用实例化后的类对象进行调用,所有该类实例化出的对象共享该静态成员函数,该函数只能访问类的静态成员变量和其它静态成员函数
class MyClass {
public:
    static void func() {}
};

int main() {
    MyClass::func();    // 推荐的方式
    MyClass obj;
    obj.func();         // 不推荐的方式,但也是允许的
}
  • static 修饰没有定义在类中的普通函数时,用于限制函数的作用域,使其只能在当前源文件中使用,不会暴露给其他文件
  • static用于限制访问,让函数或变量只能在定义它的文件中使用,避免其他文件引用到它
  • extern用于扩展访问,让声明变量或函数在其他地方定义,允许跨文件或模块访问这些符号
  • 内联函数在程序的编译期间在使用位置展开,一般是简短且频繁使用的函数,减少函数调用产生的消耗,增加代码执行效率
  • 一个项目中,我们经常使用 _函数名 表示该函数是某个函数的辅助 / 子函数
  • 为了减少内碎片,在内存对齐时申请的内存越大,所需要的对齐数就应该越大
  • 标注的进阶版的内容都是跟我这样的普通人想不出来的,主要是要学习其思想
  • 经过RoundUp函数后的size都是对齐过的,有内碎片的size
  • 桶锁是每个桶都有一个,所以声明在SpanList中
  • batchNum是理论上要分配的内存结点的数量
  • actualNum是指定位置自由链表中实际拥有的数量内存结点的数量

ObjectPool.h

包含内容:为T类型的对象开辟空间T* New()和回收空间void Delete()

#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*
};

        对于这块代码推荐再看一遍高并发内存池(一):项目介绍与定长内存池的实现-CSDN博客,因为执行的顺序是经过有点跳,毕竟是不断完善的

ConcurrentAlloc.h

包含内容:

  • 为线程分配ThreadCache的ConcurrentAlloc函数
  • 回收线程的ThreadCache的ConcurrentFree函数
#pragma once
#include "Common.h"
#include "ThreadCache.h"

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

//向内存空间申请ThreadCache
static void* ConcurrentAlloc(size_t size)
{
	//通过TLS方法,每个线程可以无锁的获取自己专属的ThreadCache对象
    //第一次来的时候该线程没有ThreadCache,需要new一个
	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);
}

相关知识点补充:

  • 线程局部存储TLS是一种变量的存储方法,这个变量在它所在的线程内是安全可访问的,但是不能被其它线程访问,这样就保持了数据的线程独立性
  • 高并发内存池项目是为了避免使用new和free的,所以这里的new ThreadCache也是暂时的,后续会将其进行替换,但本质都是申请内存

ThreadCache.h

包含内容:

  • 相关函数声明
  • ThreadCache的桶结构
  • 保证每个线程独享一个ThreadCache的TLS变量pTLSThreadCache 
#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);//(桶位置,申请的内存的大小)
private:
	FreeList _freeLists[NFREELIST];//构成如图片中所示的ThreadCache有208个桶的基本结构
};

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

CentralCache.h

包含内容:

  • 相关函数声明
  • CentralCache的桶结构
  • 饿汉模式实现的CentralCache
#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);

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

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

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

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

相关知识点补充:

  • 单例模式(Singleton Pattern)是一种设计模式,旨在确保一个类只有一个实例,并提供一个全局访问点。通常用于管理全局状态、资源或配置信息。单例模式的关键在于控制实例化的时机和方式,而懒汉模式和饿汉模式则分别代表了不同的实例化策略。

  • 懒汉模式(Lazy Initialization)
    特点:延迟实例化,即在首次需要使用实例时才进行创建。
    实现方式:通过一个静态成员函数来完成,该函数在需要时创建实例,并返回该实例的引用。
    优点:①资源利用率高,只有在需要时才会分配资源;②延迟加载,减少启动时间;
    缺点:线程不安全,多个线程可能同时创建多个实例,需要额外的同步机制来确保线程安全。

  • 饿汉模式(Eager Initialization)
    特点:在类加载时就实例化单例对象,而不管是否会用到。
    实现方式:通过静态成员的初始化来实现,实例在程序启动时就被创建。
    优点:①实现简单,避免了线程安全问题;②类加载时即初始化,适合实例创建开销较小且使用频繁的场景;
    缺点:①无法延迟加载,可能会导致不必要的资源浪费;②在某些情况下,类加载时的初始化可能导致较长的启动时间

  • 懒汉模式适用于实例化开销较大且不确定是否会使用的场景。需要注意线程安全问题,可以通过双重检查锁定(Double - Checked Locking)来解决。

  • 饿汉模式适用于实例化开销较小且会频繁使用的场景。实现简单且无需考虑线程安全问题,但在资源有限的情况下可能不适合。

PageCache.h

包含内容:

  • 饿汉模式实现的PageCache
  • 相关函数声明
  • 整体锁
#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整体上锁
private:
	SpanList _spanLists[NPAGES];

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

	static PageCache _sInst;
};

ThreadCache.cpp

包含内容:

  • 从当前线程的ThreadCache中为当前线程分配内存结点的Allocate函数
  • 回收当前线程使用完的内存结点的Deallocate函数
  • 向CentralCache申请新的内存结点的FetchFromCentralCache函数
#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);//其余分配的结点插入ThreadCache中的指定位置
		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);
}

CentralCache.cpp

包含内容:

  • 为ThreadCache分配内存结点的FetchRangeObj函数
  • 获取一个非空span的GetOneSpan函数
#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
	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的地址即可
}

PageCache.cpp

包含内容:从PageCache中获取一个新的非空span的NewSpan函数

#include "PageCache.h"

PageCache PageCache::_sInst;

//从PageCache中获取一个新的非空span
Span* PageCache::NewSpan(size_t k)
{
	assert(k > 0 && k < NPAGES);
	
	//先检查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);

			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中直接分配了,不需要再考虑该返回什么
	//可以代码复用,递归消耗的资源很小
}
  • 直接返回NewSpan(k)是因为在项目中,重点在于代码复用,且递归也只有一次该次中循环次数最多只有128次,消耗也不大,如果不代码复用重新去写几行新的代码很麻烦 

UnitTest.cpp

包含内容:两线程并发申请内存的实验函数Alloc1、Alloc2和TLSTest

#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();
}

int main()
{
	//TLSTest();//测试TLS是否正常使用
	TestConcurrentAlloc();
}

单进程单span

介绍:因为申请的字节数6、8、1、7太小且都可以按8字节对齐,所以负责分配内存结点的是ThredCache中对齐字节数为8的自由链表,对齐后每个结点的起始地址相差为8字节,只需要一个管理一页的span即可

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

介绍:我们规定一个span管理的空间大小为2^13字节,循环1024次每次都申请6字节,而实际上分配的是对齐后的8字节,经历一次循环后就会用完一个span的内存空间,当我们再申请一个6字节大小的内存时就会去再申请一个span,现在我们为NewSpan增加一些辅助代码,确保在第三次执行NewSpan时会提示获取一个新的span(因为NewSpan函数的返回值还要再调用一次NewSpan所以不能是两次)

void TestConcurrentAlloc()
{	
	//尝试用完一整个span
	for (size_t i = 0; i < 1024; i++)
	{
		void* p1 = ConcurrentAlloc(6);
	}

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

内存申请流程图

注意事项:

1、_maxsize表示此次申请理论上所能提供的最大内存结点个数,上限为num = MAX_SIZE / size(num的范围为[2,512]),当实际还可以分配更多内存结点(num个)但因min函数限制(只要_maxsize一直小于num,bathcNum就会一直等于_maxsize),在下次申请同一位置的自由链表下的内存结点时多分配一个(_freeLists[index].MaxSize() += 1),但是当_maxsize的值超过num时就不会再执行++_maxsize了

2、以单进程单span中的多个小于8字节的内存申请为例,第一次申请时先从CentralCache中获取了一个8字节内存结点(当然向堆申请了,这里省略了,只需要知道此时CentralCache中index为0的位置挂了一个管理一页的span),第二次申请时给两个,有一个被拿去直接使用,剩下的一个被挂在了ThreadCache存放8字节大小内存结点的自由链表上,第三次申请时直接用第二次申请时挂起来的内存结点,第四次申请时再次从CentralCache中获取了3个内存结点,一个使用两个挂起,第五次申请用掉挂起的一个,最后还剩一个8字节的内存结点挂在存放8字节内存结点的freelist上

~over~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值