前言:经过前3期的攻坚,我们已完整实现了内存动态申请的核心模块。接下来将进入关键阶段——内存释放机制的理解与实现,这是构建完整 高并发内存池 的最后一块技术拼图。该模块完成后,项目主体架构将基本成型(达90%),后续主要聚焦于边界测试和性能调优。欢迎关注本专栏的开发者们持续追踪代码演进,共同探讨高并发内存池的工程实践优化方案。
项目专栏:高并发内存池_敲上瘾的博客-CSDN博客
个人主页:敲上瘾-CSDN博客
目录
一、ThreadCache内存释放
在ThreadCache层我们探讨过这样一个问题,某一时段某个线程可能需要大量内存,不断向span桶中申请。而用完后这些内存就被放回它自己的自由链表桶了,导致后续线程再需要向span桶申请内存时资源不足,大部分内存都堆积在这个线程自由链表桶。
为了解决这样的问题,我们需要将部分内存归还给CentralCache。那么,自由链表达到多长时需要放回span桶呢?
之前在设计应该从span桶取多少内存块比较合适时我们也有过类似的困扰,当时设计了一个慢增长算法,每个自由链表中存了一个_maxSize,这里我们就做简单一点,用_maxSize来判断,即当一个自由链表长度超过一个批量(_maxSize)应该申请的内存时,我们把自由链表的_maxSize个节点还回span桶。
首先在class ThreadCache中声明一个函数,用来把要释放的内存截下来送入CentralCache层。
作用是把list中自由链表的_maxSize个节点截下来,然后送入CentralCache层,因为需要找到对应的哈希桶,所以传入内存块大小。
然后在Deallocate函数(释放内存)中检查是否满足归还span桶的条件,如下:
void ThreadCache::Deallocate(void* obj, size_t bytes)
{
int index = SizeClass::Index(bytes);
_freelists[index].Push(obj);
if (_freelists[index].Size() >= _freelists[index].MaxSize())
{
size_t size = SizeClass::RoundUp(bytes);
ListTooLong(_freelists[index], size);
}
}
注:Size用来获取链表长度,这里就不展示了,可以用这两种方法之一:
- 直接遍历链表并计数。
- 添加并维护_size成员函数,Size()中直接返回_size。
因为每次使用Size函数都遍历链表会影响效率,所以这里选用添加_size成员的方法。
接下来实现ListTooLong函数:
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
void* start = nullptr;
void* end = nullptr;
list.PopRange(start, end, list.MaxSize());
//交给CentralCache
CentralCache::GetInstance()->ReleaseListToSpans(start, size);
}
同样的使用start和end指针去取内存,在class FreeList里封装一个函数PopRange,如果你看懂了之前内存申请的代码,这里的实现就显得很简单,不在一一讲解。代码示例:
//取一段内存
void PopRange(void* start, void* end, int num)
{
assert(num <= _size);
start = end = _list_head;
for (int i = 0; i < num - 1; i++)
{
end = Nextobj(end);
}
_list_head = Nextobj(end);
Nextobj(end) = nullptr;
_size -= num;
}
二、CentralCache内存回收
小节目标:ReleaseListToSpans(start, size)的实现
CentralCache内存回收的主要任务就是把ThreadCache送来的内存连接到span桶中,其次,同样的问题,什么时候送到下一层呢?这个问题我们探讨过,就是当span中的所有内存块都回收回来时。
记住:理解为什么要span中的所有内存块回收回来后送入下一层这一点很重要,在下文会讲到。
回收的内存插入到Span的自由链表中
现在有一个问题:内存块要插入到哪个span?我们知道的size仅能找到它所在的哈希桶,也就是一个span链,要具体到某个span节点是一个很让人头疼的问题。
注意:不能粗鲁地把内存块连接到一个随便找的span,这样的话,属于不同页的内存块就混乱在一起,后面的PageCache就没法玩了!
追溯到本质(思考内存块从“出生”到现在的这个过程):内存块连接到某个span,实际上是要找到内存块属于那个页号,然后连接到这个页号对应的span中。
页号成为了它们之间的脐带,内存块地址除以8KB就是它所在的页,而一个span的信息中也能找到它管理着哪些页,只需要做一个页号到span的映射(即哈希表)就能解决问题。页级别的内存管理是在PageCache,我们把哈希映射在PageCache层创建和维护。
在PageCache中添加成员:
- unordered_map<PAGE_ID, Span*> _idSpanMap;
_idSpanMap在哪里填写呢?答:在页分配时。在向系统申请内存和给span分配页前是不能确定页号和span的对应关系的,所以哈希映射是随程序运行的动态填写过程。
将要被连接到CentralCache层的span,需要把它管理的所有页都映射到span中,因为拆开的内存块能获取到的是一个确定的页号。放回PageCache的span只需要它管理的开始页号和尾页号映射到span,用于span合并时对相邻页的查找。
如下:
Span* PageCache::NewSpan(size_t k)
{
assert(k > 0 && k < NPAGES);
if (!_spanLists[k].Empty())
{
return _spanLists[k].PopFront();
}
for (int i = k + 1; i < NPAGES; i++)
{
//切割页(即页分配)
if (!_spanLists[i].Empty())
{
Span* nSpan = _spanLists[i].PopFront();
Span* kSpan = new Span;
kSpan->_n = k;//页数
nSpan->_n -= k;
//填写页号
kSpan->_pageId = nSpan->_pageId;
nSpan->_pageId += k;
//建立映射
_idSpanMap[nSpan->_pageId] = nSpan;
_idSpanMap[nSpan->_pageId + nSpan->_n - 1] = nSpan;
for (PAGE_ID i = 0; i < kSpan->_n; i++)
{
_idSpanMap[kSpan->_pageId + i] = kSpan;
}
_spanLists[nSpan->_n].PushFront(nSpan);
kSpan->_isUse = true;//表示span被CentralCache使用,在下文会讲到。
return kSpan;
}
}
//向系统申请内存
Span* span = new Span;
void* ptr = SystemAlloc(NPAGES - 1);
span->_pageId = (PAGE_ID)ptr>>PAGE_SHIFT;
span->_n = NPAGES - 1;
_spanLists[span->_n].PushFront(span);
return NewSpan(k);
}
对于使用内存块查找span我们封装一个函数,如:
Span* PageCache::MapObjectToSpan(void* obj)
{
PAGE_ID index = (PAGE_ID)obj >> PAGE_SHIFT;
auto ret = _idSpanMap.find(index);
if (ret == _idSpanMap.end())
{
assert(false);
return nullptr;
}
else
{
return ret->second;
}
}
接下来我们来实现ReleaseListToSpans(start, size),它的核心工作:
- 找到每个内存块对应的span。
- 把内存块连接到span的自由链表中。
- 判断内存块是否全部回收,如果是,就将span送入PageCache中。
注意:找到哈希桶后需要加桶锁。
对于第3点,在把span送入PageCache之前,需要把span从CentralCache移除,并把span前后指针指向空,自由链表指向空。其次走到这一步意味着程序将离开CentralCache层进入PageCache层,在此之前把桶锁解了,方便让其他线程使用,然后加锁执行PageCache的内存回收函数,解锁,然后再次申请桶锁。
代码示例:
void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
void* p = start;
//找到每个p所在Span并连接到自由链表
//加桶锁
int index = SizeClass::Index(size);
_spanLists[index]._mtx.lock();
while (p)
{
void* next = Nextobj(p);
Span* span = PageCache::GetInstance()->MapObjectToSpan(p);
Nextobj(p) = span->_freeList;
span->_freeList = p;
span->_useCount--;
if (span->_useCount == 0)
{
//移出Span链表
_spanLists->Erase(span);
span->_freeList = nullptr;
span->_next = span->_prev = nullptr;
//交给PageCache回收
_spanLists[index]._mtx.unlock();
PageCache::GetInstance()->_pageMtx.lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->_pageMtx.unlock();
_spanLists[index]._mtx.lock();
}
p = next;
}
_spanLists[index]._mtx.unlock();
}
内存块全部回收后送入PageCache的作用:
内存块回收到span后,它在自由链表中的排布已经不像初次从PageCache取出那样是有序的,而是混乱的。等span全部内存块回收后,就可以把它们看做一个整体,尽管内存块的排布是混乱的也没关系,这只是逻辑上的混乱,回到PageCache层后它就是一个大块页。等PageCache再次分页时,内存块次序恢复和虚拟地址一样,增加高速缓存命中率。
三、PageCache内存回收
小节目标:ReleaseSpanToPageCache(span)的实现。
当PageCache拿到span后不要直接连接到哈希桶里,而把它前后能合并的页都合并在一起组成一个大块的页,然后再连接到哈希桶,这样可以缓解内存外碎片。
注意 是把它前后的页的span合并在一起,只有连续页的span才能合并,而不是前后桶的span,前后页的span也无法依据桶来找,要找到管理它前后页的span需要用哈希映射。
但又会带来一些问题,哈希映射得到的span可能是在CentralCache中,也有可能是在PageCache中,因为我们用的是同一个哈希映射,而合并span,指的是PageCache中的span,要如何解决?
这里我们选择在span中添加成员变量_isUse来判断span是否分配给了CentralCache,这样就可以把它们区分开。先暂时这样写,后期再做优化。比如一个span的_isUse初始状态为false,分配给CentralCache后设为true,PageCache的回收处理后设为false。
合并条件:
- 它相邻的页在哈希映射中。
- 该页不被_CentralCache使用。
- 两个span的页数相加不超过128。
合并过程:
- 用来储存合并后结果的span:页号更新为两个span中较小的页号,页数更新为两者的页数和。
- 另一个span:从PageCache的哈希桶中移除,delete删除,释放空间。
合并结果处理:
- 按上面逻辑把前后页合并得到的span,_isUse设为false,将其连接到PageCache的哈希桶,然后做页到span的哈希映射。
代码示例:
void PageCache::ReleaseSpanToPageCache(Span* span)
{
PAGE_ID n = span->_pageId;
//向前合并
while (true)
{
PAGE_ID prev = span->_pageId - 1;
auto ret = _idSpanMap.find(prev);
if (ret != _idSpanMap.end()&&!ret->second->_isUse)
{
if (span->_n + ret->second->_n >= NPAGES)
break;
span->_pageId = ret->second->_pageId;
span->_n += ret->second->_n;
_spanLists->Erase(ret->second);
delete ret->second;
}
else break;
}
//向后合并
while (true)
{
PAGE_ID prev = span->_pageId + 1;
auto ret = _idSpanMap.find(prev);
if (ret != _idSpanMap.end() && !ret->second->_isUse)
{
if (span->_n + ret->second->_n >= NPAGES)
break;
span->_n += ret->second->_n;
_spanLists->Erase(ret->second);
delete ret->second;
}
else break;
}
span->_isUse = false;
_spanLists->PushFront(span);
_idSpanMap[span->_pageId] = span;
_idSpanMap[span->_pageId + span->_n - 1] = span;
}
四、源码
需要源码的小伙伴到我的gitee上取:
内存释放/PageCache · 敲上瘾/ConcurrentMemoryPool - 码云 - 开源中国
非常感谢您能耐心读完这篇文章。倘若您从中有所收获,还望多多支持呀!