高性能内存池(四)----CentralCache实现
1.前言
在整个项目框架中,中心缓存十分重要,起到承上启下的作用,它同时负责给线程缓存分配内存和从页缓存申请内存。
2.CentralCache架构
在第一篇中我们已经初步了解了CentralCache的架构了,其中每个桶,即SpanList都有一个互斥锁。也就是桶锁
那么span到底是什么结构呢?
Span结构定义在common.h中,是由多个连续页组成的大块内存跨度(Span翻译过来就是跨度)
// 管理多个连续页大块内存跨度结构
struct Span
{PAGE_ID _pageId = 0; // 大块内存起始页的页号size_t _n = 0; // 页的数量Span* _next = nullptr; // 双向链表的结构Span* _prev = nullptr;size_t _objSize = 0; // 切好的小对象的大小size_t _useCount = 0; // 切好小块内存,被分配给thread cache的计数,//如果为0说明全被还回来了,此时就可以把这个Span还给PageCachevoid* _freeList = nullptr; // 切好的小块内存的自由链表bool _isUse = false; // 是否在被使用
};
SpanList同样也在common.h中,是一个双向带头循环链表
// 带头双向循环链表
class SpanList
{
public:SpanList(){//双向链表_head = new Span;_head->_next = _head;_head->_prev = _head;}
//第一个Span* Begin(){return _head->_next;}Span* End(){return _head;}bool Empty(){return _head->_next == _head;}void PushFront(Span* span){Insert(Begin(), span);}Span* PopFront(){Span* front = _head->_next;Erase(front);return front;}
//Insert可以复用到Pushvoid Insert(Span* pos, Span* newSpan){assert(pos);assert(newSpan);Span* prev = pos->_prev;// prev newspan posprev->_next = newSpan;newSpan->_prev = prev;newSpan->_next = pos;pos->_prev = newSpan;}
//Erase复用到Popvoid Erase(Span* pos){assert(pos);assert(pos != _head);// 1、条件断点// 2、查看栈帧/*if (pos == _head){int x = 0;}*/Span* prev = pos->_prev;Span* next = pos->_next;prev->_next = next;next->_prev = prev;}private:Span* _head;
public:std::mutex _mtx; // 桶锁
};
由于CentralCache是多线程共享的,所以要设计成单例模式:
//CentralCache.h// 单例模式
class CentralCache
{
public://单例static CentralCache* GetInstance(){return &_sInst;}// 获取一个非空的spanSpan* GetOneSpan(SpanList& list, size_t byte_size);// 从中心缓存获取一定数量的对象给thread cachesize_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);// 将一定数量的对象释放到span跨度void ReleaseListToSpans(void* start, size_t byte_size);
private:SpanList _spanLists[NFREELIST];//将构造和拷贝构造私有防止创建新的中心缓存
private:CentralCache(){}CentralCache(const CentralCache&) = delete;//直接在这里把对象建出来static CentralCache _sInst;
};
3.ThreadCache申请内存和CentralCache申请内存
CentralCache与ThreadCache的交互无非两种,一种是ThreadCache向CentralCache申请内存,另一种就是还回内存
ThreadCache申请内存
在线程缓存实现时,我们写了FetchFromCentralCache接口但是还没具体实现里面的FetchRangeObj(),
中心缓存需要先找到对应哈希桶,然后找到一个非空span结构,然后把这个span中切好的小块内存分配给线程缓存
// 从中心缓存获取一定数量的对象给thread cache
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
//找到对应哈希桶size_t index = SizeClass::Index(size);_spanLists[index]._mtx.lock();//获取一个SpanSpan* span = GetOneSpan(_spanLists[index], size);assert(span);assert(span->_freeList);// 从span中获取batchNum个对象// 如果不够batchNum个,有多少拿多少,实际拿了actualNumstart = span->_freeList;end = start;size_t i = 0;size_t actualNum = 1;while (i < batchNum - 1 && NextObj(end) != nullptr){end = NextObj(end);++i;++actualNum;}span->_freeList = NextObj(end);//双向循环链表end的下一个是头NextObj(end) = nullptr;//形成单链表结构span->_useCount += actualNum;//申请出去了多少个_spanLists[index]._mtx.unlock();return actualNum;
}
那如果CentralCache内存不足该怎么办?答案是继续向PageCach申请
CentralCache申请内存
如果ThreadCache申请内存时,对应哈希桶中无span,此时就需要CentralCache向PageCach申请一个span。
//CentralCache.cpp// 获取一个非空的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;}}// 先把central cache的桶锁解掉,这样如果其他线程释放内存对象回来,不会阻塞list._mtx.unlock();// 程序走到这里说明没有空闲span了,只能找page cache要PageCache::GetInstance()->_pageMtx.lock();//具体接口留给PageCach实现Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));span->_isUse = true;span->_objSize = size;PageCache::GetInstance()->_pageMtx.unlock();// 对获取span进行切分,不需要加锁,因为这会还没链接到SpanList,其他线程访问不到这个span// 计算span的大块内存的起始地址和大块内存的大小(字节数),这里PAGE_SHIFT是13,2*13=8K//划起来,后面要考char* start = (char*)(span->_pageId << PAGE_SHIFT);size_t bytes = span->_n << PAGE_SHIFT;char* end = start + bytes;// 把大块内存切成自由链表链接起来// 1、先切一块下来去做头,方便尾插span->_freeList = start;start += size;void* tail = span->_freeList;int i = 1;while (start < end){++i;NextObj(tail) = start;tail = NextObj(tail); // tail = start;start += size;}NextObj(tail) = nullptr;// 切好span以后,需要把span挂到桶里面去的时候,再加锁list._mtx.lock();//头插list.PushFront(span);return span;
}
需要注意的是,这里我们使用了span的页号来锁定其地址,代码中左移13位即为8*1024。
我们使用用页数来控制其大小,这里我们一页是8K
4. CentralCache回收内存
CentralCache回收内存
首先,当线程缓存中哈希桶中小块内存的个数大于该线程缓存一次性向中心缓存申请的小块内存的个数时,线程缓存会把这些还给中心缓存的span。
中心缓存的一个哈希桶中可能不止一个span,我们怎么知道哪个小块儿内存对应到哪个span?
很显然,随意将在小块儿内存还回到任意span肯定是不对的。在前面申请内存时,我们提到过如何通过地址得到页号,所以我们可以用一个unordered_map来存储页号和span的映射关系,当线程缓存还回小块儿内存时,可以通过计算小块儿内存在地址空间的页号,进而在哈希表中找到对应的span!
//CentralCache.cpp
void CentralCache::ReleaseListToSpans(void* start, size_t size)
{size_t index = SizeClass::Index(size);_spanLists[index]._mtx.lock();while (start){void* next = NextObj(start);
//PageCach的 页号-span 映射接口Span* span = PageCache::GetInstance()->MapObjectToSpan(start);NextObj(start) = span->_freeList;span->_freeList = start;span->_useCount--;// 说明span的切分出去的所有小块内存都回来了// 这个span就可以再回收给page cache,pagecache可以再尝试去做前后页的合并if (span->_useCount == 0){_spanLists[index].Erase(span);span->_freeList = nullptr;span->_next = nullptr;span->_prev = nullptr;// 这时把桶锁解掉,因为我们已经把这个span从SpanList上取下来了_spanLists[index]._mtx.unlock();// 释放span给page cache时,使用page cache的锁就可以了PageCache::GetInstance()->_pageMtx.lock();//这里PageCache的回收接口交给PageCache实现PageCache::GetInstance()->ReleaseSpanToPageCache(span);PageCache::GetInstance()->_pageMtx.unlock();_spanLists[index]._mtx.lock();}start = next;}_spanLists[index]._mtx.unlock();
}
5.总结
总的来说,CentralCache是一个承上启下的角色,负责线程缓存的内存分配和内存回收,以及向页缓存申请内存并在合适时还回去,巧妙之处在于这里使用了桶锁,这大大提升了高并发下的性能。
关于MapObjectToSpan的实现,敬请期待PageCache篇!