C++高并发内存池实战:CentralCache层
本文介绍了高并发内存池中CentralCache层的设计与实现。首先分析了Span对象的结构,包含页号、页数、前后指针等成员。然后设计了SpanList双向链表类,实现插入删除等操作,并采用单例模式创建CentralCache类。重点实现了三个关键函数:FetchRangeObj()用于分配内存给ThreadCache层,GetOneSpan()从PageCache层申请新内存,ReleaseListToSpans()管理ThreadCache层释放的内存。整个过程需要考虑多线程安全问题,通过加锁机制避免竞

前言
在续《C++高并发内存池实战:ThreadCache层》文章分布后,这篇文章将着重对CentralCache层进行讲解,并实现ThreadCache层中所未实现的函数
Span对象
在了解CentralCache层之前,我将对存在于CentralCache层和PageCache层的哈希桶内的Span类型进行讲解。首先为什么要设计Span类型?因为CentralCache层需要存储一个形似于ThreadCache层的链表,且CentralCache层的链表需要设计为双向链表(PS:为方便遍历寻找空闲的地址进行分配,所以设计为双向链表。由于单链表已经满足ThreadCache层管理内存的目的,再增加指针会加大分配的内存空间),所以Span对象应该存在两个指针,分别指向上一个节点和下一个节点的指针。在此基础上,用于CentralCache层所接收的Page层分配的内存大小都以页为单位,所以需要定义一个变量存储页号。且每一个Span用于下标不同,所以指向的分配内存也会不同,当一个Span指向的内存大于一页时,我们还需要定义存储页数的变量,通过这两个变量我们还可以快速定位内存的区间。用于Span管理的是多块内存,所以需要使用一个链表来存储管理的内存,还需要一个变量存储已经分配的内存块数量,故Span类型的设计如下:
//由分析而设计的Span结构体struct Span{ PAGE_ID _pageId = 0; //起始页的页号 size_t _n = 0; //页的数量 Span* _next = nullptr; //指向上一个Span节点 Span* _prev = nullptr; //指向下一个Span节点 size_t _useCount = 0; //已经被分配出去的内存数量 void* _freeList = nullptr;//管理内存的自由链表};为什么通过设计的_pageId和_n变量可以快速定位内存的区间?例如已知_pageId为100,而该Span的页的数量为1,那么该Span的内存区间为100*8kb到101*8kb(假设一页为8kb)。针对上述代码,也可以通过下图直观的了解到Span类型的结构:

图1.Span结构详解图
CentralCache层
根据上一小节我们知道,CentralCache层存储的对象是一个一个的哈希桶,而哈希桶内存储的对象是一个类型为Span的双向链表,所以我们也得设计一个类似于ThreadCache的单向链表一样的变量存储这个双向链表,而且这个单向链表的类型还需要具备有进栈,出栈,插入和删除的功能。对比ThreadCache层,我们设计CentralCache层还需要考虑多线程情况下的死锁问题,根据文章《C++高并发内存池实战:初步了解》可知,在设计CentralCache层时我们还需要对桶进行锁的定义,所以CentralCache层的设计如下:
class SpanList{public: SpanList(){ //初始化单向链表 _head = new Span; _head->_next = _head; _head->_prev = _head; } void PushFront(Span* span){ //进行入栈操作 Insert(Begin(), span); } Span* PopFront(){ //进行出栈操作 Span* front = _head->_next; Erase(front); return front; } void Insert(Span* pos, Span* newSpan){ //进行插入操作 assert(pos); assert(newSpan); Span* prev = pos->_prev; prev->_next = newSpan; newSpan->_prev = prev; newSpan->_next = pos; pos->_prev = newSpan; } void Erase(Span* pos){ //进行删除操作 assert(pos); assert(pos != _head); Span* prev = pos->_prev; Span* next = pos->_next; prev->_next = next; next->_prev = prev; }private: Span* _head; //指向双向链表的头节点public: std::mutex _mtx;//桶锁};为了更直观的理解SpanList,我将对SpanList进行画图分析,详细参考下图:
图2.SpanList结构详解
在设计完Span类型还有SPalList单向链表后,我们在CentralCache层的功能还没有实现,那就是返回内存给ThreadCache层的接口函数,还有就是当CentralCache层不存在多余的内存时,我们应该通过一个接口来向PageCache层进行内存的申请。而且CentralCache层不像ThreadCache层一样,针对每一个线程都存在一个ThreadCache,而CentralCache层只存在一个,所以我们需要使用单例的设计模式,针对上述讲到的要求,我们设计了CentralCache层的结构详见下述代码:
class CentralCache{public: static CentralCache* GetInstance(){ //返回唯一的CentralCache对象 return &_sInst; } //向PageCache层申请内存 Span* GetOneSpan(SpanList& list,size_t size) //返回内存至ThreadCache层的接口函数 size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size); //管理ThreadCache层释放的内存 void ReleaseListToSpans(void* start, size_t size) private: SpanList _spanLists[NFREELIST]; //哈希桶 private: //单例模式(饿汉模式) CentralCache(){} //禁止使用构造函数 CentralCache(CentralCache&) = delete; //禁止使用拷贝构造函数 static CentralCache _sInst; //定义全局的CentralCache单例对象};代码中为什么CentralCache类型的构造函数设为私有,且没有声明delete,还是注释为禁用构造函数?这是因为将CentralCache类型的构造函数设为私有,外部无法调用,即不能初始化CentralCache类型的对象。为了方便直观的理解CentralCache层的结构,可参考下图:

图3.CentralCache层结构图
在了解CentralCache层的整体构造后,我们需要对其上述代码中的FetchRangeObj()函数和GetOneSpan()函数进行实现,详见下述代码:
//CentralCache层获取一定数量的内存返回给ThreadCache层size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size){ size_t index = SizeClass::Index(size); //根据请求的内存块大小size来确定哈希桶的索引 _spanLists[index]._mtx.lock(); //对调用的哈希桶进行加锁,防止死锁 Span* span = GetOneSpan(_spanLists[index], size); //从哈希桶中获取一个Span对象 assert(span); assert(span->_freeList); //从span中获取batchNum个对象 start = span->_freeList; end = start; int i = 0; int actulNum = 1; while (i < batchNum - 1 && NextObj(end) != nullptr){ //从双向链表中获取多个内存块 end = NextObj(end); ++i; ++actulNum; } span->_freeList = NextObj(end); //获取下一节点的地址 NextObj(end) = nullptr; span->_useCount += actulNum; _spanLists[index]._mtx.unlock(); //对分配的哈希桶进行解锁 return actulNum;}//哈希桶中获取一个Span对象Span* CentralCache::GetOneSpan(SpanList& list,size_t size){ Span* begin = list.Begin(); while (begin != list.End()){ if (begin->_freeList != nullptr){ return begin; } else{ begin = begin->_next; } } //解锁,如果其他线程释放内存就不会阻塞 list._mtx.unlock(); //假如CentralCache层没有多余的内存需要去PageCache层中申请资源 PageCache::GetInstance()->_pageMtx.lock(); //解锁 Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size)); span->_isUse = true; //修改Span的状态为以分配 PageCache::GetInstance()->_pageMtx.unlock(); //对PageCache上锁 char* start = (char*)(span->_pageId << PAGE_SHIFT); //获取分配的span的起始地址 size_t bytes = span->_n << PAGE_SHIFT; //获取搭配的span大小 char* end = start + bytes; //把span切分成对应小块内存尾插进_freeList,再插入到哈希桶中 span->_freeList = start; start += size; void* tail = span->_freeList; while (start < end){ NextObj(tail) = start; tail = NextObj(tail); start += size; } NextObj(tail) = nullptr; list._mtx.lock(); //把span挂到桶里面去的时候加锁 list.PushFront(span); return span;}在实现CentralCache层分配内存后,我们还需要接受上一层所释放的内存。由于PageCache分配的内存都是连续的以页为单位的内存,而这些页在CentralCache层时会被切成一小块,在ThreadCache释放内存时,我们也应该按照切成小块的顺序进行释放,如果乱序的化会导致原本已经可以返回给PageCache层的内存,由于没有拼接为一页导致不能即时返还。所以在返回地址时,我们还需要对ThreadCache释放的内存按序来释放,确保不会产生过多的碎片空间,实现代码如下:
//管理ThreadCache层释放的内存void CentralCache::ReleaseListToSpans(void* start, size_t size){ size_t index = SizeClass::Index(size); //获取释放内存对于的哈希桶 _spanLists[index]._mtx.lock(); //对指定的哈希桶进行加锁 while (start){ void* next = NextObj(start); //获取下一个节点的指针 //返回释放内存所对应的在PageCache层的地址 Span* span = PageCache::GetInstance()->MapObjectToSpan(start); NextObj(start) = span->_freeList; //获取当前Span对象管理的内存链表 span->_freeList = start; span->_useCount--; if (span->_useCount == 0){ //当所有的Span对象都空闲时 _spanLists[index].Erase(span); span->_freeList = nullptr; span->_next = nullptr; span->_prev = nullptr; _spanLists[index]._mtx.unlock(); //解锁,以便其他线程可申请和释放资源 PageCache::GetInstance()->_pageMtx.lock(); PageCache::GetInstance()->ReleaseSpanToPageCache(span);//归还内存至PageCache PageCache::GetInstance()->_pageMtx.unlock(); _spanLists[index]._mtx.lock(); //加锁 } start = next; } _spanLists[index]._mtx.unlock();}PS:在CentralCache层进行内存的分配时需要对哈希桶进行加锁,当CentralCache层返回内存至PageCache层时则需要把对应的锁释放。在ThreadCache层对内存进行申请和释放时同样需要的对CentralCahce层的哈希桶进行加锁