【高并发内存池】释放内存 + 申请和释放总结
高并发内存池
- 1. 释放内存
- 1.1 thread cache
- 1.2 central cache
- 1.3 page cache
- 2. 申请和释放剩余补充
点赞👍👍收藏🌟🌟关注💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃
1. 释放内存
1.1 thread cache
释放内存:
- 当释放内存小于256k时将内存释放回thread cache,计算size映射自由链表桶位置i,将对象Push到_freelists[i]。
- 当链表的长度过长,则回收⼀部分内存对象到central cache。
//ConcurrentAlloc.h
static void ConcurrentFree(void* ptr,size_t size)
{assert(pTLSThreadCache);pTLSThreadCache->Deallocate(ptr, size);
}
以什么标准来判断ThradCache当前某个桶的自由链表的长度过长了呢?
我们在给ThreadCache的桶加一个Size成员,记录当前这个桶空闲内存块的个数。如果当前这个桶空闲内存块的个数大于等于这个桶下一次找CentralCache要的一批量的内存块个数。那就把这一批量从当前桶拿到然后还给CentralCache。
其实还可以给ThreadCache一个记录它占了多大内存的成员,如果这个遍历大于2M,就从上到下遍历桶还一部分空闲内存块回去,避免一个ThreadCache占据太多内存。我们这个项目的原型考虑了更多这里的细节。我们就简单一下用个Size记录当前桶空闲内存块个数就行了。
//Common.hstatic void*& NextObj(void* obj)
{return *(void**)obj;
}class Freelist
{
public://释放内存void Push(void* obj){//头插assert(obj);NextObj(obj) = _freelist;_freelist = obj;++_size;}//申请内存void* Pop(){//头删assert(_freelist);void* obj = _freelist;_freelist = NextObj(obj);--_size;return obj;}//一次挂一批void PushRange(void* start,void* end,size_t n){NextObj(end) = _freelist;_freelist = start;_size += n;}//一次拿一批void PopRange(void*& start, void*& end, size_t n){assert(_size >= n);start = end = _freelist;for (size_t i = 0; i < n - 1; ++i){end = NextObj(end);}_freelist = NextObj(end);NextObj(end) = nullptr;_size -= n;}//链表是否为空bool IsEmpty(){return _freelist == nullptr;}//获取当前桶获取下一批内存块的个数size_t& GetMaxSize(){return _maxSize;}//当前桶的空闲内存块的个数size_t GetSize(){return _size;}private:void* _freelist = nullptr;size_t _maxSize = 1;//记录当前桶下一次找ContraltCache一次要一批量是多少个内存对象size_t _size = 0;//当前桶空闲内存块个数
};
//ThreadCache.hclass ThreadCache
{
public://申请和释放内存对象void* Allocate(size_t size);void Deallocate(void* ptr, size_t size);// 从中心缓存获取对象void* FetchFromCentralCache(size_t index, size_t size);// 释放对象时,链表过长时,回收内存回到中心缓存void ListTooLong(Freelist& list, size_t size);
private:Freelist _freelists[NFREELIST];
};// TLS thread local storage 线程局部存储
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
//ThreadCache.hppvoid ThreadCache::Deallocate(void* ptr, size_t size)
{assert(ptr);assert(size <= MAX_BYTES);size_t index = SizeClass::Index(size);_freelists[index].Push(ptr);//当前桶空闲内存块个数大于等于下一次申请一批量内存块个数//就还下一批量内存块的个数给CentralCacheif (_freelists[index].GetSize() >= _freelists[index].GetMaxSize()){//从当前桶获取一批量内存块ListTooLong(_freelists[index], size);}
}void ThreadCache::ListTooLong(Freelist& list, size_t size)
{void* start = nullptr;void* end = nullptr;//获取一批量内存块对象list.PopRange(start, end, list.GetMaxSize());//将这一批内存块对象还给CentralCache对应桶下的对应Span对象CentralCache::GetInstance()->ReleaseListToSpans(start, size);
}
1.2 central cache
释放内存:
- 当thread_cache过长或者线程销毁,则会将内存释放回central cache中的,释放回来时 --use_count。当use_count减到0时则表示所有对象都回到了span,则将span释放回page cache,page cache中会对前后相邻的空闲页进行合并。
//CentralCache.h//所有线程共享,整个进程仅有一个,所有弄成单例模上:饿汉(静态对象)
class CentralCache
{
public://获取单例对象static CentralCache* GetInstance(){return &_sInst;}// 获取一个非空的spanSpan* GetOneSpan(Spanlist& list, size_t size);// 从中心缓存获取一定数量的内存块对象给thread cachesize_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);// 将一定数量的内存块对象释放到对应桶下的对应span对象中void ReleaseListToSpans(void* start, size_t size);private:CentralCache(){}CentralCache(const CentralCache&) = delete;CentralCache& operator=(const CentralCache&) = delete;
private:Spanlist _spanlists[NFREELIST];static CentralCache _sInst;
};
考虑这样一个问题,ThreadCache还回来的内存块还到CentralCache对应桶中,但是对应桶可能有多个Span对象,那这些还回来的内存块对象是属于那个Span呢?
比如说目前有两个Span对象,分别管理一页,2000是Span1管理,2001是Span2管理。下面有四个内存块还回来了?假设1、2内存块是从Span1拿的,3、4是从Span2拿的。那怎么确定对应内存块是从那个Span中拿的呢?
以1、2内存块从Span1拿的为例,既然是从Span1管理2000这一页拿,这一页是划分为固定大小的小内存块你才拿的。那1、2这个内存块的地址是不是就在FA000和FA2000之间的,拿它们的地址/8KB算出的整数还是2000。
因此我们可以用unordered_map或者map建立一个页号与Span一对一的映射关系。拿到内存块的地址/8KB 看它是那一页的,然后根据页号就可以知道它是那个Span了。
这里我们使用unordered_map查找更快一些,这个哈希桶等会PageCache也要用,因此放在PageCache里。
//PageCache.h#include"Common.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,并合并相邻的spanvoid 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一一对应关系
public:std::mutex _pageMtx; //整个锁
};
然后在处理一下,当CentralCache从PageCache拿到一个Span,返回之前,我们要把这个Span管理的页号与Span建立关系。
//从PageCache第k号桶中获取一个span对象
Span* PageCache::NewSpan(size_t k)
{assert(k > 0 && k < NPAGES);//这里加锁递归会有死锁问题,除非用递归互斥锁,还有在外面调用这个函数之前加锁// 先检查第k个桶里面有没有spanif (!_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 Cachereturn 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);//将Kspan给CentralCache之前,先将页号和span对应关系记录for (size_t i = 0; i < Kspan->_n; ++i){_idSpanMap[Kspan->_pageid + i] = Kspan;}//将管理k页的span给Central Cachereturn Kspan;}}//走到这里说明,后面的位置都没有大页的span//那就去找堆申请一个128page的spanSpan* bigSpan = new Span;void* ptr = SystemAlloc(NPAGES - 1);bigSpan->_pageid = (PAGE_ID)ptr >> PAGE_SHIFT;bigSpan->_n = NPAGES - 1;//span挂在对应桶下_spanlists[bigSpan->_n].PushFront(bigSpan);//重复上述过程寻找k号桶的span,一定还回一个spanreturn NewSpan(k);
}
在增加一个地址转成页号找到对应Span的函数
// 获取从对象到span的映射
Span* PageCache::MapObjectToSpan(void* obj)
{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;}
}
接下来CentralCache就可以把一批内存块还给PageCache了。这里也需要记住在取PageCache之前先把对应桶锁解掉。不如别的线程来对应桶上申请释放就阻塞了。
// 将一定数量的内存块对象释放到对应桶下的对应span对象中
void CentralCache::ReleaseListToSpans(void* start, size_t size)
{size_t index = SizeClass::Index(size);_spanlists[index]._mtx.lock();//遍历将内存块对象还给对应的span//如何知道一个内存块对象是从那个span对象中拿走的?//用内存块中的地址/8kb,得到对应的页号,根据页号走到对应的span//如何根据页号找到对应span,用unordered_map记录页号和span一一对应关系//因为PageCache等会也要用到这个哈希桶,因为哈希桶定义到PageCache中while (start){void* next = NextObj(start);Span* span = PageCache::GetInstance()->MapObjectToSpan(start);//将start头插进对应span中NextObj(start) = span->_freelist;span->_freelist = start;span->_usecount--;//检查当前span中切分的内存块个数是否已经被还完了//如果还完,就将该span从CentralCache中删除,然后还给PageCache//PageCache看是否能将该span前后空闲span合并,减少内存碎片if (span->_usecount == 0){//将该span从桶中删除_spanlists[index].Erase(span);span->_freelist = nullptr;span->_prev = nullptr;span->_next = nullptr;//从CentarlCache去PageCache之前先把桶锁释放//如果不释放,万一其他线程来这个桶找span申请或者释放内存块,就阻塞住_spanlists[index]._mtx.unlock();//所有线程共享PageCache,所以先加锁PageCache::GetInstance()->_pageMtx.lock();PageCache::GetInstance()->ReleaseSpanToPageCache(span);PageCache::GetInstance()->_pageMtx.unlock();//回来再把对应桶锁加上_spanlists[index]._mtx.lock();}start = next;}_spanlists[index]._mtx.unlock();
}
1.3 page cache
释放内存:
- 如果central cache释放回一个span,则依次寻找span的前后page id的没有在使用的空闲span,看是否可以合并,如果合并继续向前寻找。这样就可以将切小的内存合并收缩成大的span,减少内存碎片。
假设Span管理的页是起始页是2000,一共管理5页,和它相邻的前后Span如何找到?
和它的前一个相邻的Span最后一页就是1999,后一个相邻的Span的首先就是2005。我们还是可以通过找到页号就可以找到对应的Span。
合并相邻的页,通过页号找到相邻的页看是否能合并,如果该页的Span在PageCache说明该Span还没被使用可以合并。如果在CentralCache说明该Span正在被使用不能合并。
那如何确定一个Span是否在被使用呢?能不能用Span中的_usecount==0来判断?
不能!因为有时间间隔。如果线程thread1找PageCache拿到一个Span,记住_usecount初始可是0,然后切分成一块块小内存块,在挂到CentralCache对应桶中,然后正准备从这个Span拿一批内存块,这个时候_usecount可还是0,只有真正拿到了才会++。另一个线程thread2在PageCache合并相邻的空闲的Span,如果相邻的就是thread1正准备拿的Span,你用_usecount==0判断是否被使用,那不就造成线程安全的问题了吗!!
所以不能用Span中的_usecount==0判断一个Span是否在使用。我们再给Span加一个_isuse成员来判断Span是否在使用,拿到CentralCache的Span就在使用,在PageCache的Span就没有在使用。
//Span管理一个跨度的大块内存//管理以页为单位的大快内存
struct Span
{PAGE_ID _pageid = 0;//管理一大块连续内存块的起始页的页号size_t _n = 0;//管理几页Span* _prev = nullptr;//带头双向循环链表前指针Span* _next = nullptr;//带头双向循环链表后指针size_t _usecount = 0;//记录Span对象中自由链表挂的内存块对象使用数量void* _freelist = nullptr;//自由链表挂的是Span对象一块大连续内存块按照桶位置大小切分的一块块小的内存块bool _isuse = false;//当前Span是否被使用
};
当CentralPage从PageCache获取到一个新的Span就已经被用了,增加一句代码。
// 获取一个非空的span
Span* CentralCache::GetOneSpan(Spanlist& list, size_t size)
{//遍历对应桶中是否有span或者span中是否有内存对象Span* it = list.Begin();while (it != list.End()){if (it->_freelist){return it;}else{it = it->_next;}}//当Central Cache对应桶下面没有span,那就往下一层Page Cache找,但是首先要先把对应桶锁释放//如果再来一个线程是申请内存但是没有内存锁住也没问题,但是如果这个线程是来还内存的呢?//锁住那不就还不了了,因此往下走之前先把桶锁释放list._mtx.unlock();//走到这里说明该桶中并没有span或者span中没有内存对象,Central Cache那就找到下一层Page Cache要一个span//Page Cache 是一个按桶下标映射的哈希桶,第i号桶表示这个桶下面的span管理的都是i页page//central找page要,关注的是要的span是管理k页的span,然后就去k号桶去要//PageCache的锁是一把整锁,虽然也可以是桶锁但是消耗性能//当PageCache对应桶没有span就往下面桶继续找,就涉及多个桶加锁解锁,//因此我们在最外面直接把PageCache锁住,只加锁解锁一次即可PageCache::GetInstance()->_pageMtx.lock();Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));//在CentralCache中的span是已经被使用的spanspan->_isuse = true;//span被使用PageCache::GetInstance()->_pageMtx.unlock();//从PageCache拿上来的span是每一个线程独享的,因此切片时不需要加锁//把从PageCache拿回的span切分成size大小的一个个内存块挂在自由链表,最后再把span挂在对应的桶中//根据页号找到span管理的一大块内存的起始地址//地址用char* 方便下面地址++char* start = (char*)(span->_pageid << PAGE_SHIFT);//计算这块内存有多大size_t bytes = span->_n << PAGE_SHIFT;//结尾地址char* end = start + bytes;//使用尾插法将大内存变成大小为size的一快快小内存挂在自由链表上span->_freelist = start;start += size;void* tail = span->_freelist;while (start < end){NextObj(tail) = start;tail = start;start += size;}//自由链表最后为nullptrNextObj(tail) = nullptr;//切好span以后,需要把span挂到桶里面去的时候,再加锁list._mtx.lock();//再把span挂在对应桶list.PushFront(span);return span;
}
现在先别急着就去unordered_map去找在PageCache中相邻的Span,你现在肯定也找不到,还记得unordered_map目前记录的是什么吗?它当前只记录了在CentralCache中的Span管理的页号与Span的对应关系,也就是已经使用的Span的页号和Span的关系,根本就没有记录在PageCache中Span管理的页号SPan对应的关系。因此还要在修改一下代码。
在PageCache的Span,只需要记录它管理的首页和尾页与Span的映射关系就行了。合并我们就找相邻Span的首页和尾页就找到对应的Span了。对于在CentralCache中Span因为要被切分成一块块小内存然后才给ThreadCache用,当小内存块回来的时候,可能是这个Span中任意一页的小内块,然后/8KB,找到页号,在找到对应的SPan。因此要把Span中每一页和Span对应关系都保存。
//从PageCache第k号桶中获取一个span对象
Span* PageCache::NewSpan(size_t k)
{assert(k > 0 && k < NPAGES);//这里加锁递归会有死锁问题,除非用递归互斥锁,还有在外面调用这个函数之前加锁// 先检查第k个桶里面有没有spanif (!_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 Cachereturn 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;_idSpanMap.set(Ispan->_pageid, Ispan);_idSpanMap.set(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 Cachereturn Kspan;}}//走到这里说明,后面的位置都没有大页的span//那就去找堆申请一个128page的spanSpan* bigSpan = new Span;void* ptr = SystemAlloc(NPAGES - 1);bigSpan->_pageid = (PAGE_ID)ptr >> PAGE_SHIFT;bigSpan->_n = NPAGES - 1;//span挂在对应桶下_spanlists[bigSpan->_n].PushFront(bigSpan);//重复上述过程寻找k号桶的span,一定还回一个spanreturn NewSpan(k);
}
准备工作做完了,接下来就是CentralPage还回来一个Span,看相邻的Span是否能合并,解决内存碎片的问题。
// 释放空闲span回到Pagecache,并合并相邻的span
void PageCache::ReleaseSpanToPageCache(Span* span)
{// 如何知道相邻的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合并prevSpanspan->_pageid = prevSpan->_pageid;span->_n += prevSpan->_n;// 将pevSpan从对应的桶中删除_spanlists[prevSpan->_n].Erase(prevSpan);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()){break;}Span* nextSpan = ret->second;// 后面相邻页的span在使用,不合并了if (nextSpan->_isuse == true){break;}// 合并出超过128页的span没办法挂在桶下,不合并了if (nextSpan->_n + span->_n > NPAGES - 1){break;}span->_n += nextSpan->_n;_spanlists[nextSpan->_n].Erase(nextSpan);delete nextSpan;}//合并好的span挂在对应桶下_spanlists[span->_n].PushFront(span);span->_isuse = false;//重新映射在PageCace的Span的首页和尾页与Span映射关系_idSpanMap[span->_pageid] = span;_idSpanMap[span->_pageid + span->_n - 1] = span;
}
关于释放内存就到这里了,下面我们在对细节进行优化。
2. 申请和释放剩余补充
当要的内存小于256KB,每个线程通过TLS找到自己的ThreadCache要,如果没有就找CentralCache要,在没有就找PageCache要,在没有就找堆去要。
之前我们一直没谈的是大于256KB怎么办?
其实这里分为分成两种情况,256KB/8KB=32Page,PageCache最大的桶是128Page。因此就有 大于32Page 小于等于128Page,找PageCache要。大于128Page直接找堆要。
不管是找PageCache要还是找堆要,都是按页为单位对齐的。
//Common.hclass SizeClass
{//...static inline size_t _RoundUp(size_t bytes, size_t alignNum){return ((bytes + alignNum - 1) & ~(alignNum - 1));}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//找PageCache或者堆按页为单位对齐{return _RoundUp(size, 1 << PAGE_SHIFT);}return -1;}
};//ConcurrentAlloc.hstatic void* ConcurrentAlloc(size_t size)
{//大于256KB/8KB=13page 小于等于128page 找PageCache要//大于128page找堆要if (size > MAX_BYTES){//找PageCache或者堆要都是以页为单位对齐size_t alignnum = SizeClass::RoundUp(size);//在PageCache那个桶size_t kpage = alignnum >> PAGE_SHIFT;//在PageCache内部处理找是否找堆要PageCache::GetInstance()->_pageMtx.lock();Span* span = PageCache::GetInstance()->NewSpan(kpage);//这里不需要记录这个span已经被使用,因为还得时候合并的这个span相邻页的span看是否在使用PageCache::GetInstance()->_pageMtx.unlock();//得到地址返回void* ptr = (void*)(span->_pageid << PAGE_SHIFT);return ptr;}else//小于256KB通过三层缓存要{//通过TLS 每个线程无锁的获取自己的专属的ThreadCache对象if (pTLSThreadCache == nullptr){pTLSThreadCache = new ThreadCache;}return pTLSThreadCache->Allocate(size);}
}
这里在把找PageCache要一个Span处理一下
//从PageCache第k号桶中获取一个span对象
Span* PageCache::NewSpan(size_t k)
{assert(k > 0);//大于 128page找堆要if (k > NPAGES - 1){void* ptr = SystemAlloc(k);//申请一个span对象管理这块内存,释放内存的时候要对应的spanSpan* span = new Span;span->_pageid = (PAGE_ID)ptr >> PAGE_SHIFT;span->_n = k;_idSpanMap[span->_pageid] = span;return span;}//这里加锁递归会有死锁问题,除非用递归互斥锁,还有在外面调用这个函数之前加锁// 先检查第k个桶里面有没有spanif (!_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 Cachereturn 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;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 Cachereturn Kspan;}}//走到这里说明,后面的位置都没有大页的span//那就去找堆申请一个128page的spanSpan* bigSpan = new Span;void* ptr = SystemAlloc(NPAGES - 1);bigSpan->_pageid = (PAGE_ID)ptr >> PAGE_SHIFT;bigSpan->_n = NPAGES - 1;//span挂在对应桶下_spanlists[bigSpan->_n].PushFront(bigSpan);//重复上述过程寻找k号桶的span,一定还回一个spanreturn NewSpan(k);
}
释放也是一样,小于256KB的通过TLS找自己的ThreadCache释放,大于32Page小于等于128Page还给PageCache,大于128Page还直接返给堆。
static void ConcurrentFree(void* ptr,size_t size)
{//大于32page小于等于128page直接还给PageCache,//大于128page直接还给堆if (size > MAX_BYTES){//根据地址找转换为对应的页号,通过页号找到对应的span,在找到对应内存大小Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);PageCache::GetInstance()->_pageMtx.lock();PageCache::GetInstance()->ReleaseSpanToPageCache(span);PageCache::GetInstance()->_pageMtx.unlock();}else{assert(pTLSThreadCache);pTLSThreadCache->Deallocate(ptr, size);}
}
相关文章:

【高并发内存池】释放内存 + 申请和释放总结
高并发内存池 1. 释放内存1.1 thread cache1.2 central cache1.3 page cache 2. 申请和释放剩余补充 点赞👍👍收藏🌟🌟关注💖💖 你的支持是对我最大的鼓励,我们一起努力吧!😃&#x…...

AutoGen学习笔记系列(九)Advanced - Selector Group Chat
这篇文章瞄的是AutoGen官方教学文档 Advanced 章节中的 Selector Group Chat 篇章,介绍了SelectorGroupChat对象如何从一个Team中选择其中一个Agent与LLM进行对话,并且在得到结果后进行二次规划,同时你也可以自定义选择函数。本质上还是对Tea…...

Stream特性(踩坑):惰性执行、不修改原始数据源
在日常开发中,Stream API 提供了一种高效且易于使用的工具集来处理集合数据。 本文主要讲解 Stream 的两个特性:惰性执行,不修改原始数据源。 为什么说这两个、而不讲下其他的特性呢?主要是因为在开发中如果忽略这两个特性的话&…...

springcloud sentinel教程
QPS(Queries Per Second)即每秒查询率 TPS,每秒处理的事务数目 PV(page view)即页面浏览量 UV 访问数(Unique Visitor)指独立访客访问数 一、初识Sentinel 什么是雪崩问题? 微服务之间相…...

像素的一生 Life of a Pixel - Steve Kobes 2020版
像素的一生 Life of a Pixel - Steve Kobes 2020版 《Life of a Pixel》 作者是Google大佬 Steve Kobes 2020年 介绍Chromium内核完整渲染流程的视频,介绍的非常好,想要学习了解chromium内核渲染必看! 油管视频地址为:https://w…...

系统部署【信创名录】及其查询地址
一、信创类型 (一)服务器: 1.华为云 2.腾讯云 3.阿里云 (二)中央处理器(CPU): 1.海思,鲲鹏920服务器 (三)中间件 1.人大金仓 ࿰…...

VSCode 配置优化指南:打造高效的 uni-app、Vue2/3、JS/TS 开发环境
VSCode 配置优化指南,适用于 uni-app、Vue2、Vue3、JavaScript、TypeScript 开发,包括插件推荐、设置优化、代码片段、调试配置等,确保你的开发体验更加流畅高效。 1. 安装 VSCode 如果你还未安装 VSCode,可前往 VSCode 官网 下载最新版并安装。 2. 安装推荐插件 (1) Vue…...

C++中的析构函数
目录 一、什么是析构函数: 二、析构函数的特性: 一、什么是析构函数: C中的析构函数非常简单,它的功能无非是帮助我们自动归还堆区的空间给操作系统。当我们使用内存开辟函数(如malloc()、realloc())等&a…...

同步,异步,并发,并行
同步: 任务按顺序执行,必须等待前一个任务完成后才能开始下一个任务。 任务之间是强依赖的,通过直接调用或阻塞等待实现。 示例:读取文件时,代码会阻塞直到文件读取完成。 异步: 任务无需等待前一个任务完成即可启…...

种子填充(Floodfill、泛滥填充、洪水填充) 算法c++模板
种子填充(Floodfill) 算法: 从任意 W 开始,不停地把邻接的 W 用 . 代替。1 次 DFS 后与初始 W 连接的所有 W 都被替换成 . 了。 因此,直到图中不存在 W 为止,总共进行 DFS 的次数就是答案了。 问题: 有一个大小为 N x M 的园子,雨后积水。 8 连通的积水被认为是连接在…...

MATLAB控制函数测试要点剖析
一、功能准确性检验 基础功能核验 针对常用控制函数,像用于传递函数建模的 tf 、构建状态空间模型的 ss ,以及开展阶跃响应分析的 step 等,必须确认其能精准执行基础操作。以 tf 函数为例,在输入分子与分母系数后,理…...

【新手指南】pyqt可视化远程部署deepseek7B蒸馏版模型
本地效果:(如果想做这个的本科毕设,建议美化界面。) 总结:MobaXterm远程连接autodl服务器,在MobaXterm上利用X11转发使pyqt可视化页面在自己的电脑上展现出来。 1. 官网下载MobaXterm MobaXterm free Xse…...

大语言模型在患者交互任务中的临床使用评估框架
An evaluation framework for clinical use of large language models in patient interaction tasks An evaluation framework for clinical use of large language models in patient interaction tasks | Nature Medicine 2025.1 收到时间:2023 年 8 月 8 日 …...

DeepSeek-V3 技术报告解读
DeepSeek火了有一段时间了,春节假期因为没时间,所以关于deepseek大模型一系列的技术报告一直没看,新年开工后,抽一点时间把之前的坑补起来,关于DeepSeek-V3技术报告的解读已经有很多了,但我相信不同的人去读…...

suricata安装测试
系统版本为Ubuntu 22.04.4。 # cat /etc/issue Ubuntu 22.04.4 LTS \n \l # # uname -a Linux logging 6.8.0-49-generic #49~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Wed Nov 6 17:42:15 UTC 2 x86_64 x86_64 x86_64 GNU/Linux添加suricata的apt库。 # add-apt-repository pp…...

Java反射简单理解
Java反射是指在运行时(runtime)能够动态地获取类的内部信息,并能直接操作类的属性和方法的一种机制。通过反射,开发者可以在运行时检查类、接口、字段和方法,并且可以调用这些方法和访问这些字段,而无需在编…...

WPS Word中英文混杂空格和行间距不一致调整方案
文章目录 问题1:在两端对齐的情况下,如何删除参考文献(英文)的空格问题2:中英文混杂行间距不一致问题问题3:设置中文为固定字体,设置西文为固定字体参考 问题1:在两端对齐的情况下&a…...

探秘沃尔什-哈达玛变换(WHT)原理
沃尔什-哈达玛变换(WHT)起源 起源与命名(20世纪早期) 数学基础:该变换的理论基础由法国数学家雅克哈达玛(Jacques Hadamard)在1893年提出,其核心是哈达玛矩阵的构造。扩展与命名&…...

优雅拼接字符串:StringJoiner 的完整指南
在Java开发中,字符串拼接是高频操作。无论是日志格式化、构建CSV数据,还是生成动态SQL,开发者常需处理分隔符、前缀和后缀的组合。传统的StringBuilder虽然灵活,但代码冗余且易出错。Java 8推出的StringJoiner类,以简洁…...

AFL++安装
学习fuzzing也几天了,今天记录AFL的安装及使用 一、实验环境 虚拟机:ubuntu20.04 当然也可以uname -a去看自己的版本号 二、AFL安装 1.先更新一下工具 sudo apt update2.安装AFL必要的一些依赖,例如编译工具(如 build-essen…...

开发者社区测试报告(功能测试+性能测试)
功能测试 测试相关用例 开发者社区功能背景 在当今数字化时代,编程已经成为一项核心技能,越来越多的人开始学习编程,以适应快速变化的科技 环境。基于这一需求,我设计开发了一个类似博客的论坛系统,专注于方便程序员…...

如何优化 VS Code 远程开发环境?高效配置与性能提升策略
相关系列文章 《新手教学系列——善用 VSCode 工作区,让开发更高效》 《新手教学系列——用 VSCode 实现高效远程开发》 《Webpack 优化全攻略:彻底解决 Vue 项目 npm run dev 的内存泄露问题》 引言 随着开发环境不断进化,VS Code 作为一款轻量级的代码编辑器,已经成为…...

【二】JavaScript能力提升---this对象
目录 this的理解 this的原理 事件绑定中的this 行内绑定 动态绑定 window定时器中的this 相信小伙伴们看完这篇文章,对于this的对象可以有一个很大的提升! this的理解 对于this指针,可以先记住以下两点: this永远指向一个…...

YC 孵化项目 Pinch:实时语音翻译视频会议平台;Mistral OCR:能处理多语言多模态复杂文档丨日报
开发者朋友们大家好: 这里是 「RTE 开发者日报」 ,每天和大家一起看新闻、聊八卦。 我们的社区编辑团队会整理分享 RTE(Real-Time Engagement) 领域内「有话题的 技术 」、「有亮点的 产品 」、「有思考的 文章 」、「有态度的 …...

OSPF报文分析
OSPF报文分析 组播地址 224.0.0.0~224.0.0.255为预留的组播地址(永久组地址),地址224.0.0.0保留不做分配,其它地址供路由协议使用; 224.0.1.0~238.255.255.255为用户可用的组播地址(…...

蓝桥杯刷题周计划(第二周)
目录 前言题目一题目代码题解分析 题目二题目代码题解分析 题目三题目代码题解分析 题目四题目代码题解分析 题目五题目代码题解分析 题目六题目代码题解分析 题目七题目代码题解分析 题目八题目题解分析 题目九题目代码题解分析 题目十题目代码题解分析 题目十一题目代码题解分…...

PH热榜 | 2025-03-09
1. ResumeUp 2.0 标语:聊聊,几分钟内就能帮助你打造完美的ATS简历。 介绍:告别为写完美简历而烦恼的日子吧!只需与人工智能聊天,回答几个简单的问题,就能在几分钟内生成强有力的简历,不仅能通…...

《gradio :AI demos》
《gradio :AI demos》 Folders and files Name Last commit message Last commit date parent directory .. agent_chatbot Declare exports in __all__ for type checking (#10238) 3 months ago all_demos Fix deployed Spaces (#10271) 2 months ago …...

Interop_UdsProtocolStack之数据解析器使用方法
一、背景与需求 在汽车电子领域,UDS(Unified Diagnostic Services)协议栈的响应报文解析是实现ECU诊断功能的核心环节。本文提出一种基于配置驱动的通用解析器开发方案,满足以下行业需求: 多协议兼容性:支…...

从0开始完成基于异步服务器的boost搜索引擎
文章目录 前言一、本项目涉及的技术栈和环境二、boost是什么?三、项目的相关背景四、项目的相关原理五、正排索引 vs 倒排索引 - 搜索引擎具体原理六、 编写数据去标签与数据清洗的模块 Parser6.1 下载boost的文档库6.2 去标签6.3 代码编写 七、索引模块7.1. 整体框…...