高并发内存池 - 释放内存过程联调

高并发内存池 - 释放内存过程联调

项目 gitee 链接: 高并发内存池项目

项目 github 链接: 高并发内存池项目

我们先将测试函数展现出来:

cpp 复制代码
#include "Comm.h"
#include "ConcurrentAlloc.h"
void Alloc1()
{
    void* ptr1 = ConcurrentAlloc(1);
    void* ptr2 = ConcurrentAlloc(2);
    void* ptr3 = ConcurrentAlloc(3);
    void* ptr4 = ConcurrentAlloc(4);
    void* ptr5 = ConcurrentAlloc(5);
    void* ptr6 = ConcurrentAlloc(6);
    void* ptr7 = ConcurrentAlloc(7);
    void* ptr8 = ConcurrentAlloc(8);


    ConcurrentFree(ptr1, 1);
    ConcurrentFree(ptr2, 2);
    ConcurrentFree(ptr3, 3);
    ConcurrentFree(ptr4, 4);
    ConcurrentFree(ptr5, 5);
    ConcurrentFree(ptr6, 6);
    ConcurrentFree(ptr7, 7);
    ConcurrentFree(ptr8, 8);


    return;
}

int main()
{
    Alloc1();
    return 0;
}

这里至少回收 7 次才会最终进入 Page Cache 层中尝试合并 Span,下面分析一下为什么:

在申请内存的过程中,自由链表的 MaxSize() 涨到 5,代表只有自由链表的空间资源个数大于 5 时,才会从 Thread Cache 层中进入 Central Cache 层中,这个信息我们之后会用到。

接下来分析一下 Central Cache 的 Span 分出了多少空间资源出去,它的 _useCount 是多少?

申请次数 来源 CentralCache 实际分配数 累计 useCount
1 CentralCache Fetch 1 1
2 CentralCache Fetch 2 3
3 ThreadCache 自由链表(上次剩的) 0 3
4 CentralCache Fetch 3 6
5 ThreadCache 自由链表 0 6
6 ThreadCache 自由链表 0 6
7 CentralCache Fetch 4 10

所以 7 次申请后,CentralCache 里那个 1 页 Span 的 useCount = 10

下面进入释放流程:

Deallocate 中,只有当自由链表长度 >= MaxSize(=5) 时,才会调用 ListTooLong 向 CentralCache 归还一批(一次归还 5 个)。

假设只做了 7 次申请(第 7 次 Fetch 后 ThreadCache 自由链表剩余 3 个):

释放次数 释放后 ThreadCache 长度 是否触发 ListTooLong 本次归还后 CentralCache useCount
1 3 + 1 = 4 ---
2 4 + 1 = 5 第一次 10 - 5 = 5
3 0 + 1 = 1 ---
4 1 + 1 = 2 ---
5 2 + 1 = 3 ---
6 3 + 1 = 4 ---
7 4 + 1 = 5 第二次 5 - 5 = 0 ⬅️ 进入 PageCache
  • 第 2 次释放:第一次凑够 5 个,归还 5 个给 CentralCache,useCount 从 10 降到 5,Span 还在被使用,不会进入 PageCache。

  • 第 7 次释放:第二次凑够 5 个,再归还 5 个,useCount 从 5 降到 0。此时 CentralCache 发现这个 Span 已经完全空闲,将其从桶中摘下,调用 RealeaseSpanToPageCache。

所以总结一下:为什么第七次释放才进入 Page Cache 中?

进入 PageCache 的充要条件是 CentralCache 中某个 Span 的 useCount 减到 0

而 useCount 从 10 清到 0 需要经过 两轮 ListTooLong 归还(每轮 5 个):

  • 第一轮发生在第 2 次释放
  • 第二轮发生在第 7 次释放

因此第 7 次回收(释放)时,对象才最终离开 CentralCache 进入 PageCache,与 PageCache 中剩余的 127 页 Span 前后合并,恢复成一个完整的 128 页大 Span。


接下来讲解我在联调过程中遇到的 BUG:

1. 编译运行问题

在对程序排错的过程中有个问题困扰了我

cpp 复制代码
void ThreadCache::Deallocate(void* ptr, size_t size) //归还空间
{
    assert(ptr);  //归还的空间不能为空指针
    assert(size <= MAX_BYTES);  //归还空间的大小不能超过 MAX_BYTES
    std::cout << size << std::endl;
    size_t index = SizeClass::Index(size); //犯错,原先传入的为 index
    _freeList[index].Push(ptr);
 
    if (_freeList[index].Size() >= _freeList[index].MaxSize()) //如果自由链表的长度大于了一次批量申请的数量
    {
        ListTooLong(_freeList[index], size);
    }
}

在我将传入的 index 错误参数更改为 size 后,传入的参数理应为 1(在 ConcurrentFree(ptr1, 1) 中),但进入 Index 函数中查看时,却总是发现 bytes 的值始终为 8,我十分不解。

cpp 复制代码
static inline size_t Index(size_t bytes)
{
    //断言申请的空间是否大于 256 KB
    assert(bytes <= 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;
}

它与我代码中的另一个错误强强联合

cpp 复制代码
static inline size_t _Index(size_t bytes, size_t align_shift)
	{
		return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1; //这里原先并没有 -1
	}

所以造成了传入 0 ~ 7 结果为 0,一旦传入 8 结果为 1,但这并不符合对齐的规则,申请空间大小为 8 字节时,对应的是 0 号桶,所以发现这个错误后我进行了修正,但参数传递的问题还在,即使最后我显示传入 size1 变量,定义为 1,在 Index 中仍然看到传入值为 8,就像这样:

cpp 复制代码
void ThreadCache::Deallocate(void* ptr, size_t size) //归还空间
{
    assert(ptr);  //归还的空间不能为空指针
    assert(size <= MAX_BYTES);  //归还空间的大小不能超过 MAX_BYTES
    std::cout << size << std::endl;
    int size1 = 1;
    size_t index = SizeClass::Index(size1); //犯错,原先传入的为 index
    _freeList[index].Push(ptr);
 
    if (_freeList[index].Size() >= _freeList[index].MaxSize()) //如果自由链表的长度大于了一次批量申请的数量
    {
        ListTooLong(_freeList[index], size);
    }
}

最后我看到 index 未初始化时值一直为 8,我突然想起来,我之前有一版错误代码一直传入的是 index,而 index 的值恰好为 8,自那以后一直按的是 F5 进行调试,基于旧版本的运行程序在观测,没有重新编译生成新的代码,所以自然传入值一直为 8。

到此一切水落石出,所以一定要养成随改随编的习惯,这也警醒了我自己。

2. 无法合并成大页的 128 Span

在我联调进入 Page Cache 层中,我发现走到最后 Span 合并时,始终无法合并成 128 页完整的 Span,这与推测是相悖的,所以代码的逻辑一定出现了问题,我先排查将 Span 放入哈希表的部分,因为如果此处没出错,那对应页号一定在 Span 中 ,但在调试代码时,我发现对应页号在哈希表中未找到,所以才跳出循环,此处一定出现问题。

果不其然,我将 spanK 的首末页号挂入了哈希表中,spanK 是要分配给 Central Cache 层的 Span,而 spanN 才是空闲在 Page Cahce 层中,要将首末页号挂入哈希表中的 Span。

cpp 复制代码
Span* PageCache::NewSpan(size_t k)
{
    assert(k > 0 && k < NPAGES);
    //先检查第 k 个桶中是否还有 Span
    if (!_spanLists[k].Empty())
        return _spanLists[k].PopFront();  //猜的第三个坑,没有加上 [k]
    
    //走到这里说明桶中为空,要遍历 Page Cache 是否有大页 Span 可分割
    for (int i = k; i < NPAGES; i++)
    {
        if (!_spanLists[i].Empty())    
        {
            Span* spanN = _spanLists[i].PopFront();
            Span* spanK = new Span;                 //踩的第一个坑,没有申请空间,野指针直接使用

            spanK->_pageId = spanN->_pageId;
            spanK->_n = k;
            
            spanN->_pageId += k;
            spanN->_n -= k;
            
            _spanLists[spanN->_n].PushFront(spanN);  //踩的第三个坑,把 spanK 挂上去了
            
            //将 SpanN 的首页号和末页号存入哈希表中,方便归还合并
            _idSpanMap[spanK->_pageId] = spanK;                  //这里踩坑,把 SpanK 挂上去了
            _idSpanMap[spanK->_pageId + spanK->_n - 1] = spanK;

            //将要分配给 Thread Cache 的 Span 建立页号和地址的映射,方便归还时寻找对应地址
            for (int i = 0; i < spanK->_n; i++)
            {
                _idSpanMap[spanK->_pageId + i] = spanK;
            }
            return spanK;
        }
    }

    //走到这里说明也没有大页 Span,所以此时需要向堆申请空间
    Span* bigSpan = new Span;
    void* ptr = SystemAlloc(NPAGES - 1);      //申请一个最大页数的 Span
    bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;  //踩的第二个坑,左移右移逻辑踩坑
    bigSpan->_n = NPAGES - 1;

    _spanLists[bigSpan->_n].PushFront(bigSpan);
    
    return NewSpan(k);  //递归调用自己,此时一定有大页 Span 可供分割
}

在将这次错误更正后,可以确定哈希表中一定存在 Span 的页号,但最后仍未合并成 128 页的 Span,此时问题极可能出在查找本身逻辑中,在仔细排查下,我发现了最终的问题:

cpp 复制代码
PAGE_ID nextPage = span->_pageId + span->_n + 1;  //这里踩坑,多加了一个 1

在计算下一 Span 的首页号时,多加了一个 1,所以在哈希表中没有对应的页号,无法查找到。

在将这个问题解决后,程序运行正常结束,无崩溃。

即使在多线程的情况下,代码也可以成功运行:

cpp 复制代码
#include "Comm.h"
#include "ConcurrentAlloc.h"
void Alloc1()
{
    void* ptr1 = ConcurrentAlloc(1);
    void* ptr2 = ConcurrentAlloc(2);
    void* ptr3 = ConcurrentAlloc(3);
    void* ptr4 = ConcurrentAlloc(4);
    void* ptr5 = ConcurrentAlloc(5);
    void* ptr6 = ConcurrentAlloc(6);
    void* ptr7 = ConcurrentAlloc(7);
    void* ptr8 = ConcurrentAlloc(8);


    ConcurrentFree(ptr1, 1);
    ConcurrentFree(ptr2, 2);
    ConcurrentFree(ptr3, 3);
    ConcurrentFree(ptr4, 4);
    ConcurrentFree(ptr5, 5);
    ConcurrentFree(ptr6, 6);
    ConcurrentFree(ptr7, 7);
    ConcurrentFree(ptr8, 8);


    return;
}

void Alloc2()
{
    for (int i = 0; i < 5; i++)
        void* ptr = ConcurrentAlloc(7);
}

void Thread()
{
    std::thread t1(Alloc1);
    std::thread t2(Alloc1);
    t1.join();
    t2.join();
}

int main()
{
    Thread();
    // Alloc1();
    // Alloc2();
    return 0;
}

在释放内存过程联调结束后,我们还会对项目的一些点进行优化,评估性能,让我们下期再见👋。

相关推荐
天空'之城3 天前
C 语言工业级通用组件手写 06:固定块内存池
c语言·嵌入式开发·内存池·底层优化
天空'之城6 天前
C 语言工业级通用组件 02:通用内存池
c语言·嵌入式·内存管理·内存池
UrSpecial2 个月前
从零实现Nginx风格内存池
nginx·内存池
君鼎2 个月前
内存池完整实现——C++20版
c++20·内存池
不吃土豆的马铃薯2 个月前
5.SGI STL 二级空间配置器 _S_chunk_alloc核心函数解析
开发语言·c++·vscode·c·内存池
不吃土豆的马铃薯2 个月前
4.SGI STL 二级空间配置器 allocate 与_S_refill 源码解析
c语言·开发语言·c++·dreamweaver·内存池
June`2 个月前
高并发内存池如何实现
c++·tcmalloc·内存池
YYYing.2 个月前
【C++项目之高并发内存池 (五)】一些小细节和性能优化及整体测试
c++·性能优化·高并发·内存池·基数树
YYYing.2 个月前
【C++项目之高并发内存池 (四)】三层缓存的空间回收流程详解
c++·笔记·缓存·高并发·内存池