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

目录

查缺补漏

问题: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 _WIN32void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);//包含<windows.h>文件才有它们三个的定义以及使用VirtualAlloc函数
#endifif (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://构造初始的SpanListSpanList(){_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 posvoid 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 nextvoid 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获取一个非空的spanSpan* 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页的spanSpan* 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越小上限越高,最高是512size_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个内存结点,若不够就拿实际拥有的数量actualNumstart = 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寻找一个非空的spanSpan* 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中获取一个新的非空spanPageCache::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头插前要上锁,防止其它线程同时方位当前的SpanListlist._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页的spanSpan* 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()
{	//尝试用完一整个spanfor (size_t i = 0; i < 1024; i++){void* p1 = ConcurrentAlloc(6);}//如果用完了一个新的span那么p2指向的地址应该是上一个用完的span的结尾地址void* p2 =  ConcurrentAlloc(8);cout << p2 <<endl;
}

~over~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/148052.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

谷歌收录批量查询,怎么查看批量查询谷歌收录情况

在SEO&#xff08;搜索引擎优化&#xff09;领域&#xff0c;确保网站内容被谷歌等搜索引擎有效收录是提升网站可见性和流量的关键步骤。批量查询谷歌收录情况&#xff0c;能够帮助网站管理员快速了解哪些页面已被搜索引擎识别并编入索引&#xff0c;哪些页面可能存在问题需要优…

【python】石头剪刀布,模拟十次并统计获胜次数

解决问题 下面是一个使用Python编写的剪刀、石头、布游戏的程序&#xff0c;包含玩家与计算机对战和模拟计算机对战10次的功能。 import random def get_computer_choice(): return random.randint(0, 2) def get_user_choice(): choice input("请输入剪刀(0)…

Spring高手之路24——事务类型及传播行为实战指南

文章目录 1. 编程式事务&#xff08;不推荐&#xff09;2. 声明式事务&#xff08;推荐&#xff09;3. 事务的传播行为&#xff08;复杂混合事务场景及时序图说明&#xff09;3.1 NESTED和REQUIRES_NEW传播行为的区别 1. 编程式事务&#xff08;不推荐&#xff09; 定义&#…

MAC激活Typora以及禁止成功激活弹窗的方法

激活 Typora 首先在官网下载 Typora 的最新版 并且安装。 打开以下目录 /Applications/Typora.app/Contents/Resources/TypeMark/page-dist/static/js/ 注意在 Applications 中&#xff0c;需要对 Typora 右键选择 Show Packages Contents 即可进入 Typora.app。 在该目录的文…

C++自动驾驶面试核心问题整理

应用开发 概述&#xff1a;比较基础&#xff0c;没啥壁垒&#xff0c;主要有linux开发经验即可 问题&#xff1a;基础八股&#xff0c;如计算机网络、操作系统、c11等基础三件套&#xff1b;中等难度算法题1-2道。 中间件开发&#xff08;性能优化&#xff09; 概述&am…

快递物流查询-快递查询-快递单号查询-快递物流单号查询-快递物流轨迹查询-快递物流查询接口

快递物流查询接口&#xff08;API&#xff09;是一种允许开发者通过编程方式实时查询快递物流信息的服务。这些接口通常集成了多家快递公司的物流数据&#xff0c;为电商平台、物流管理系统、个人用户等提供便捷的物流查询服务。以下是关于快递物流查询接口的一些详细介绍&…

哪有什么三教九流,物以类聚罢了——kmeans聚类算法

观察人类社会&#xff0c;亦或说车水马龙中的光怪陆离&#xff0c;不难发现《马原》中介绍的人类社会中的个体&#xff0c;总是通过某种方面的“类似”聚在一起&#xff0c;文学上称这种现象叫做物以类聚&#xff0c;人以群分。 一.引言 前文提到&#xff0c;每个数据项&#x…

SpringBoot项目License证书生成与验证(TrueLicense) 【记录】

SpringBoot项目License证书生成与验证(TrueLicense) 【记录】 在非开源产品、商业软件、收费软件等系统的使用上&#xff0c;需要考虑系统的使用版权问题&#xff0c;不能随便一个人拿去在任何环境都能用。应用部署一般分为两种情况&#xff1a; 应用部署在开发者自己的云服务…

Qt笔记(十七)cmake编译Qt项目

Qt笔记&#xff08;十七&#xff09;cmake编译Qt项目 1. 文件内容与文件结构1.1.文件目录1.2. CMakeLists.txt内容1.3. main.cpp文件1.4. mouseevent.h1.5. mouseevent.cpp1.6. 生成Visual Studio项目后编译报错1.7. 界面显示中文乱码问题 1. 文件内容与文件结构 1.1.文件目录…

jdk11特性介绍

JDK 11&#xff08;也称为Java 11&#xff09;是Java平台的一个重要版本&#xff0c;它引入了许多新特性和改进&#xff0c;旨在提高开发者的生产力和Java平台的性能。以下是一些JDK 11的主要特性&#xff1a; 局部变量类型推断&#xff08;Local-Variable Syntax for Lambda P…

2009考研数学真题解析-数二:

第一题&#xff1a; 解析&#xff1a;先找间断点&#xff1a;分母不能等于0&#xff0c;分母是sinΠx&#xff0c; 因此不难看出间断点是x0&#xff0c;-1&#xff0c;-2&#xff0c;-3。。。。。 接着一个一个来算这些点是什么间断点。 &#xff0c;从x趋于2开始&#xff0c;分…

JavaScript是如何来的~~

文章目录 前言一、网络的诞生 ( The birth of the Web )二、Mosaic 浏览器三、Netscape 浏览器四、JavaScript的诞生 ~ 千呼万唤始出来总结 前言 例如&#xff1a;想要了解一门语言的发展历程&#xff0c;首先你得知道它是怎么来的&#xff0c;所以本文开篇介绍了网络的基本发…

智能BI平台项目

1.项目介绍 BI商业智能&#xff1a;数据可视化、报表可视化系统 4&#xff09;发布订阅 Resource 是基于名称进行查找的&#xff0c;而Spring框架中更常用的 Autowired 则是基于类型进行查找的。如果找不到匹配的bean&#xff0c;Autowired 会抛出异常&#xff0c;而 Resource…

EAGLE——探索混合编码器的多模态大型语言模型的设计空间

概述 准确解释复杂视觉信息的能力是多模态大型语言模型 (MLLM) 的关键重点。最近的研究表明&#xff0c;增强的视觉感知可显著减少幻觉并提高分辨率敏感任务&#xff08;例如光学字符识别和文档分析&#xff09;的性能。最近的几种 MLLM 通过利用视觉编码器的混合来实现这一点…

网络层协议 —— IP协议

目录 0.前言 1.IP协议的格式 2.IP地址 2.1IP地址的划分 国际间IP地址的划分 公有IP 私有IP 特殊的IP地址 国内IP地址的划分 2.2IP地址不足问题 2.3IP地址的功能 2.4如何使用IP地址 2.5IP地址的构成 3.网段划分 以前的方案 现在的方案 4.认识宏观网络 5.路由 …

SpringCloud config native 配置

SpringCloud config native 配置 1.概述 最近项目使用springCloud 框架&#xff0c;使用config搭建git作为配置中心。 在私有化部署中&#xff0c;出现很多比较麻烦的和鸡肋的设计。 每次部署都需要安装gitlab 有些环境安装完gitlab&#xff0c;外面不能访问&#xff0c;不给开…

QT实现升级进度条页面

一.功能说明 在Qt中实现固件升级的进度条显示窗口&#xff0c;你可以通过创建一个自定义的对话框&#xff08;Dialog&#xff09;来完成。这个对话框可以包含一个进度条&#xff08;QProgressBar&#xff09;、一些文本标签&#xff08;QLabel&#xff09;用于显示状态信息&am…

SSL 最长签发时间是多久?

在当今数字化的时代&#xff0c;网络安全变得至关重要。为了确保数据在网络传输中的安全性&#xff0c;SSL&#xff08;Secure Sockets Layer&#xff0c;安全套接层&#xff09;证书被广泛应用。那么&#xff0c;SSL最长签发时间是多久呢&#xff1f; SSL证书是一种数字证书&…

差分数组介绍

差分数组 差分数组介绍定义性质性质1: 计算数列第i项的值性质2: 计算数列第i项的前缀和应用场景差分数组具体示例【leetcode】370.区间加法题目描述题解【leetcode】1109. 航班预订统计题目描述题解【leetcode】2848.与车相交的点题目描述题解差分数组介绍 定义 对于已知有n个…

C#如何把写好的类编译成dll文件

1 新建一个类库项目 2 直接改写这个Class1.cs文件 3 记得要添加Windows.Forms引用 4 我直接把在别的项目中做好的cs文件搞到这里来&#xff0c;连文件名也改了&#xff08;FilesDirectory.cs&#xff09;&#xff0c;这里using System.Windows.Forms不会报错&#xff0c;因为前…