高并发内存池: 编写

发布于:2024-04-28 ⋅ 阅读:(25) ⋅ 点赞:(0)

主要在于:内存的申请内存的释放(归还)

申请内存

释放内存

一.申请内存

ThreadCache

  • Allocate: 拿一个小块内存给程序.1.先找到对应的freeList拿 2.找CentralCache拿
  • FetchFromCentralCache:向CentralCache索要"批量".若要到了多个将多的挂在对应的freeList

CentralCache

  • FetchRangeObj: 获取批量小块内存给ThreadCache, .
    • 若不够批量则有多少拿多少, 返回实际拿到的小块内存数量  
    • 小块内存来自Span对象--调用GetSpan
  • GetSpan: 获取一个Span对象 1.在SpanList里拿 2. 找PageCache拿
    • 将拿到的大块内存切分成小块, 并挂到Span对应的freeList
    • 返回拿到的Span对象

PageCache

  • NewSpan: 1.在当前桶里拿 2. 找后面的桶拿 3.找堆拿
  • 拿到的Span, 需要记录PageID与Span的映射关系.后面回收小块还给对应的Span
  • 在后面桶拿到的Span要做切割, 额外还要记录剩下Span首尾页与Span的映射.后面合并相邻页
  • 返回这个Span对象

二.释放内存

ThreadCache

  • 找到对应的_freeList, 头插
  • 若_freeList过长, 还一段内存给CentralCache. 调用ListToLong

CentralCache

  • ReleaseListToSpans.根据之前PageID与Span建立的映射关系, 找到Span,进行头插
  • 若use_count == 0. 说明所有小块内存回来了. 将其还给PageCache. 

PageCache

  • ReleaseSpanToPageCache: 根据之前建立的映射关系(首尾页).合并相邻的页 + 记录该Span首尾页与Span的映射关系. 方便其它来合并

三.细节及优化

使用基数树来建立PageID与Span对象的映射会更快

  •  读取的时候可以不加锁
    • 基数树首先就开好了空间(写入操作不会改结构). 所以读的时候不用加锁

    • 而使用hash表存其映射, 若进行写, 可能回导致底层结构变化(扩容),所以必须加锁

  • 高度更低(1~3层)
    • 32位下, 记录页数 = 2^32 / 一页的大小(2^13) = 2^19. 能直接开出来, 但64位就不能

    • 第一二层时预先分配的(尽可能覆盖大多数地址空间) 第三层按需分配来节省空间

四.可能的BUG预防

内存泄漏

  • 由于还内存不是还_freeList的所有小块对象(也不该还所有), 所以可能导致部分内存块一直挂在_freeList上.(但是进程结束之后, 会自动释放, 就没有内存泄漏了). 
  • 若程序不会退出, 则考虑ThreadCache上的内存块到一定大小, 调用函数, 将其归还

双重释放

  • 外部使用的时候, 释放完将指针置nullptr, 内部有assert断言操作(为nullptr直接报错)
  • 使用智能指针管理内存,配合定制删除器
#include <iostream>
#include <memory>

// 自定义new和free函数
void* myNew(size_t size) {
    return ConcurrentAlloc(size);
}

void myFree(void* ptr) {
    ConcurrentFree(ptr);
}

// 定义自定义删除器
struct CustomDeleter {
    void operator()(int* p) {
        if (p) myFree(p);
    }
};

int main() {
    // 使用智能指针管理动态分配的内存
    std::shared_ptr<int> ptr(static_cast<int*>(myNew(sizeof(int))), CustomDeleter());
    return 0;
}