LinuxC++项目开发日志——高并发内存池(4-central cache框架开发)
central cache
- CentralCache(中心缓存)概念
- 一、CentralCache 的内存申请流程
- 二、CentralCache 的内存释放流程
- 高并发内存池代码更新
- common.h通用
- SizeClass类
- Span类
- SpanList 类
- Thread cache类
- Central cache类
CentralCache(中心缓存)概念
CentralCache 是高并发内存池的中间层,承上启下(衔接 ThreadCache 与 PageCache),核心采用哈希桶结构,其哈希桶的映射规则与 ThreadCache 完全一致(即相同大小的内存块对应相同的哈希桶索引),但每个哈希桶位置挂载的不是直接的内存块,而是 SpanList(Span 链表)—— 每个 Span 管理一段连续内存,且已按对应哈希桶的内存块大小,切割成多个小内存块,这些小内存块通过 Span 内部的自由链表(_freelist)组织。
一、CentralCache 的内存申请流程
当 ThreadCache 中某一大小的内存块耗尽时,会向 CentralCache 批量申请内存,具体流程如下:
1. ThreadCache 批量申请与 CentralCache 加锁处理
ThreadCache 采用慢开始反馈调节算法确定批量申请的内存块数量(类似 TCP 拥塞控制):
-
初始时申请数量较少(避免内存浪费),若该大小内存块的需求持续,申请数量会逐步增长(直至上限);
-
且内存块越小,单次申请数量越多,内存块越大,单次申请数量越少。
-
CentralCache 接收申请后,首先找到对应大小的哈希桶(通过内存块大小计算索引),并对该哈希桶加桶锁(仅锁定当前桶,不影响其他桶的操作,最大化并发效率)。
-
从该哈希桶的 SpanList 中,找到一个拥有空闲内存块的 Span,从其自由链表(_freelist)中取出对应数量的小内存块,交付给 ThreadCache。
-
每向 ThreadCache 交付一个内存块,该 Span 的 _usecount(已分配对象计数)就加 1(用于跟踪 Span 内存的使用状态)。
2. 向 PageCache 申请新 Span(当当前桶无空闲内存时)
若当前哈希桶的 SpanList 中,所有 Span 都没有空闲内存块(即自由链表为空),则 CentralCache 需要向更上层的 PageCache 申请一个新的 Span:
-
向 PageCache 申请一段连续内存(以页为单位),得到一个新的 Span(包含页 ID、页数量等信息)。
-
根据当前哈希桶对应的内存块大小,将新 Span 管理的连续内存切割成多个等大的小内存块,并将这些小内存块通过 Span 的自由链表(_freelist)串联起来。
-
将切割好的 Span 加入当前哈希桶的 SpanList 中,之后再从该 Span 的自由链表中取出对应数量的内存块,交付给 ThreadCache,并更新 _usecount。
二、CentralCache 的内存释放流程
当 ThreadCache 中的内存块需要释放时(如 ThreadCache 某一哈希桶的自由链表过长,或线程销毁时),会将内存块归还给CentralCache,具体流程如下:
1. ThreadCache 归还内存与 Span 计数更新
-
ThreadCache 将待释放的小内存块,归还给 CentralCache 对应大小的哈希桶(通过内存块大小计算索引),并对该桶加桶锁。
-
找到该内存块所属的 Span(通过内存地址反向映射到对应的 Span),将内存块重新接入 Span 的自由链表(_freelist)。
-
每归还一个内存块,该 Span 的 _usecount(已分配对象计数)就减 1。
2. Span 归还给 PageCache(当 Span 完全空闲时)
-
当某一 Span 的 _usecount 减至 0 时,说明该 Span 管理的所有小内存块都已归还,Span 处于完全空闲状态:
-
CentralCache 将这个完全空闲的 Span 从当前哈希桶的 SpanList 中移除,解除桶锁后,将其归还给 PageCache。
-
PageCache 接收 Span 后,会检查该 Span 的前后是否存在其他空闲 Span,若存在则进行页合并(将连续的空闲页合并成一个更大的 Span),以减少内存碎片,便于后续分配更大的内存块。
高并发内存池代码更新
common.h通用
#pragma once // 防止头文件被重复包含#include <iostream>
#include <string>
#include <vector>
#include <cstdlib>
#include "Log.hpp" // 日志模块头文件
#include "Lockguard.hpp" // 锁保护模块头文件using std::cout;
using std::endl;
using namespace LogModule; // 使用日志模块的命名空间
using namespace LockGuardModule; // 使用锁保护模块的命名空间// 自由链表的总数量:208个桶,对应不同大小的内存块
static constexpr size_t NFREELISTS = 208;// 线程缓存能处理的最大内存大小:256KB
// 超过这个大小的内存申请直接走中央缓存或页堆
static constexpr size_t MAX_BYTES = 256 * 1024;// 如果是Windows 32位或64位系统(_WIN32是Windows平台的预定义宏)
#ifdef _WIN32// 在Windows系统上,将PageID定义为size_t类型// size_t在Windows中通常与指针位数一致(32位系统为4字节,64位系统为8字节)typedef size_t PageID;
#else// 对于非Windows系统(如Linux、macOS等类Unix系统)// 将PageID定义为unsigned long long类型(固定8字节,可表示更大范围的页编号)typedef unsigned long long PageID;
#endif/*** @brief 获取内存块中的下一个对象指针* @param ptr 当前内存块的指针引用* @return void*& 下一个对象的指针引用* * 这个函数用于实现自由链表的连接功能。它将内存块的前8字节(在64位系统)* 用作存储下一个内存块的地址,实现链式结构。* * 工作原理:* 1. 将ptr强制转换为void**(指向指针的指针)* 2. 解引用得到void*&(指针的引用)* 3. 这样就可以直接修改下一个节点的地址*/
inline void*& NextObj(void*& ptr)
{return *static_cast<void**>(ptr);
}/*** @brief 不可拷贝的基类* * 这个类提供了禁止拷贝语义的基础功能,同时允许移动语义。* 常用于需要防止意外拷贝的资源管理类(如单例、资源句柄等)。* * 设计特点:* 1. 保护构造函数:只能被派生类访问* 2. 删除拷贝构造函数和拷贝赋值运算符:防止拷贝* 3. 默认移动构造函数和移动赋值运算符:允许移动* 4. 虚析构函数:确保正确的多态销毁*/
class NonCopyable
{
protected:// 保护构造函数,只能被派生类实例化NonCopyable() = default;// 虚析构函数,确保派生类正确析构virtual ~NonCopyable() = default;// 删除拷贝操作,防止对象拷贝NonCopyable(const NonCopyable&) = delete;NonCopyable& operator=(const NonCopyable&) = delete;// 允许移动操作,支持资源转移NonCopyable(NonCopyable&&) = default;NonCopyable& operator=(NonCopyable&&) = default;
};
SizeClass类
SizeClass 是高并发内存池的内存大小管理工具类,核心职责是实现「内存大小对齐」「对齐后大小映射哈希索引」「批量申请数量计算」三大功能,通过分区间、差异化的对齐规则,将内存碎片率控制在 10% 左右,同时为 ThreadCache 和 CentralCache 的哈希桶映射提供统一逻辑。
运行
#pragma once
// 引入依赖头文件:日志工具、通用配置、基础类型定义
#include "Log.hpp"
#include "common.h"
#include <cstddef>/*** 内存大小对齐规则说明(核心目标:控制内碎片浪费≤10%)* 按需求内存大小划分 5 个区间,每个区间采用不同的对齐粒度,对应不同的自由链表组:* 1. 区间 [1, 128] 字节:8 字节对齐 → 对应 freelist 索引 [0, 16) → 共 16 个链表* 2. 区间 [129, 1024] 字节:16 字节对齐 → 对应 freelist 索引 [16, 72) → 共 56 个链表* 3. 区间 [1025, 8192] 字节(8*1024):128 字节对齐 → 对应 freelist 索引 [72, 128) → 共 56 个链表* 4. 区间 [8193, 65536] 字节(64*1024):1024 字节对齐 → 对应 freelist 索引 [128, 184) → 共 56 个链表* 5. 区间 [65537, 262144] 字节(256*1024):8192 字节对齐(8*1024)→ 对应 freelist 索引 [184, 208) → 共 24 个链表
*/// SizeClass 类:封装内存大小对齐、索引计算、批量申请数量计算的静态方法(无需实例化)
class SizeClass
{
public:// 核心方法1:将输入的内存大小 bytes 向上对齐到对应区间的标准大小// 返回值:对齐后的标准大小(失败返回 -1)static inline int RoundUp(size_t bytes){// 区间1:[1, 128] 字节 → 8字节对齐if(betweenNum(bytes, 1, 128)){return _RoundUp(bytes, 8);}// 区间2:[129, 1024] 字节 → 16字节对齐(注意:betweenNum 是左闭右开,故上限用 1024)else if(betweenNum(bytes, 128+1, 1024)){return _RoundUp(bytes, 16);}// 区间3:[1025, 8192] 字节 → 128字节对齐(8*1024=8192)else if(betweenNum(bytes, 1024+1, 8*1024)){return _RoundUp(bytes, 128);}// 区间4:[8193, 65536] 字节 → 1024字节对齐(64*1024=65536)else if(betweenNum(bytes, 8*1024+1, 64*1024)){return _RoundUp(bytes, 1024);}// 区间5:[65537, 262144] 字节 → 8192字节对齐(256*1024=262144)else if(betweenNum(bytes, 64*1024+1, 256*1024)){return _RoundUp(bytes, 1024*8);}// 超出支持的最大区间(>256KB)→ 输出警告并返回 -1else {Log(WARNING) << "size invalid!";return -1;}}// 核心方法2:根据输入的内存大小 bytes,计算其对齐后大小对应的自由链表索引// 返回值:自由链表的索引(失败返回 -1)static inline int Index(size_t bytes){// 各区间对应的自由链表数量(与注释中的链表数量一一对应)// groupnum[0]:区间1的链表数(16);groupnum[1]:区间2的链表数(56);以此类推std::vector<int> groupnum = {16, 56, 56, 56, 14};// 区间1:[1, 128] 字节 → 计算该区间内的子索引(align_shift=3 对应 2^3=8 字节对齐)if(betweenNum(bytes, 1, 128)){return _Index(bytes, 3);}// 区间2:[129, 1024] 字节 → 先减去区间1的最大值(128),再算子索引,最后加上前一区间的总链表数(16)else if(betweenNum(bytes, 128+1, 1024)){return _Index(bytes-128, 4) + groupnum[0]; // align_shift=4 对应 2^4=16 字节对齐}// 区间3:[1025, 8192] 字节 → 减去区间2的最大值(1024),算子索引,加上前两区间总链表数(16+56=72)else if(betweenNum(bytes, 1024+1, 8*1024)){return _Index(bytes-1024, 7) + groupnum[0] + groupnum[1]; // align_shift=7 对应 2^7=128 字节对齐}// 区间4:[8193, 65536] 字节 → 减去区间3的最大值(8192),算子索引,加上前三区间总链表数(16+56+56=128)else if(betweenNum(bytes, 8*1024+1, 64*1024)){return _Index(bytes-8*1024, 10) + groupnum[0] + groupnum[1] + groupnum[2]; // align_shift=10 对应 2^10=1024 字节对齐}// 区间5:[65537, 262144] 字节 → 减去区间4的最大值(65536),算子索引,加上前四区间总链表数(16+56+56+56=184)else if(betweenNum(bytes, 64*1024+1, 256*1024)){return _Index(bytes-64*1024, 13) + groupnum[0] + groupnum[1] + groupnum[2] + groupnum[3]; // align_shift=13 对应 2^13=8192 字节对齐}// 超出支持的最大区间 → 输出警告并返回 -1else {Log(WARNING) << "size invalid!";return -1;}}// 核心方法3:计算 ThreadCache 向 CentralCache 批量申请内存块的数量(慢开始算法的基础)// 逻辑:内存块越小,单次申请数量越多;内存块越大,单次申请数量越少,同时限制上下限(2~512)static size_t NumMoveSize(size_t bytes){// 合法性检查:字节数为0 或 超过 ThreadCache 支持的最大字节数(MAX_BYTES,通常为256KB)if(bytes == 0 || bytes > MAX_BYTES){Log(LogModule::WARNING) << "bytes invalid!";return 0;}// 基础计算:用最大支持字节数(MAX_BYTES)除以当前字节数,得到理论申请数量(确保总申请内存不超标)size_t ret = MAX_BYTES / bytes;// 下限:单次申请数量至少2个(避免频繁申请)if(ret < 2){ret = 2;}// 上限:单次申请数量最多512个(避免一次性申请过多内存导致浪费)if(ret > 512){ret = 512;}return ret;}private:// 辅助方法1:通用的向上对齐实现(仅支持 align 为 2 的幂次方的情况)// 公式逻辑:(bytes + align - 1) 先将 bytes 补到下一个 align 的倍数,再通过 & (~(align-1)) 清除低位冗余static size_t _RoundUp(size_t bytes, size_t align){return (bytes + align - 1) & (~(align - 1));}// 辅助方法2:计算某区间内的子索引(基于对齐粒度的移位运算,align_shift 是对齐粒度的2的幂次)// 逻辑:等价于 (向上取整(bytes / 对齐粒度) ) - 1(索引从0开始)// 示例:bytes=9,align_shift=3(8字节对齐)→ (9+8-1)>>3 = 16>>3=2 → 2-1=1 → 子索引1static size_t _Index(size_t bytes, size_t align_shift){return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;}// 辅助方法3:判断数值 num 是否在 [low, high) 区间内(左闭右开,适配区间划分逻辑)static bool betweenNum(int num, int low, int high){return (num >= low && num < high) ? true : false;}
};
Span类
#pragma once
#include "common.h"
#include <cstddef>struct Span
{// 页ID,标识该Span管理的内存页在内存池中的唯一编号// 初始值为0,表示未分配具体的页PageID _pageid = 0;// 该Span所管理的内存页数量// 例如:_n = 5 表示当前Span管理了5个连续的内存页size_t _n = 0;// 双向链表指针,指向下一个Span节点Span* _next = nullptr;// 双向链表指针,指向前一个Span节点// (_next和_prev用于将多个Span节点组织成双向链表,便于管理)Span* _prev = nullptr;// 该Span管理的内存块大小(每个小块的字节数)// 例如:_size = 16 表示该Span将内存页分割成多个16字节的小块size_t _size = 0;// 已使用的内存块数量// 用于跟踪该Span中当前被分配出去的小块数量size_t _usecount = 0;// 指向空闲内存块链表的头指针// 管理当前Span中所有未被使用的小块内存void* _freelist = nullptr;// 标记当前Span是否正在被使用// true表示该Span中有内存块被分配使用,false表示完全空闲bool _isUse = false;
};
SpanList 类
SpanList 是高并发内存池中的Span 链表管理类,基于双向循环链表(带头节点) 实现,核心职责是组织和管理 Span 结构体(内存页管理单元),同时内置互斥锁确保多线程环境下的操作安全,主要为 CentralCache 和 PageCache 提供 Span 的增删查等基础操作。
运行
#pragma once
// 引入依赖头文件:日志工具、Span结构体定义、通用配置、标准互斥锁库
#include "Log.hpp"
#include "Span.hpp"
#include "common.h"
#include <mutex>// SpanList 类:管理Span的双向循环链表,继承NonCopyable禁止拷贝(避免链表所有权混乱)
class SpanList : public NonCopyable
{
public:// 构造函数:初始化双向循环链表(创建头节点,头节点的前后指针均指向自身)SpanList(){_head = new Span; // 创建头节点(不存储实际Span数据,仅用于链表遍历)_head->_next = _head; // 头节点的next指针指向自身(循环特性)_head->_prev = _head; // 头节点的prev指针指向自身(循环特性)}// 迭代器相关:获取链表的起始位置(头节点的下一个节点,即第一个有效Span)Span* begin(){return _head->_next;}// 迭代器相关:获取链表的结束位置(头节点本身,作为遍历终止标志)Span* end(){return _head;}// 头部插入:将Span节点插入到链表头部(头节点之后)void PushFront(Span* span){insert(_head, span); // 调用insert方法,在头节点后插入新节点}// 头部删除:从链表头部弹出一个Span节点(头节点的下一个节点)Span* PopFront(){Span* span = _head->_next; // 获取头节点的下一个节点(待弹出节点)if (span == _head) // 若待弹出节点是头节点,说明链表为空{return nullptr; // 返回空指针(删除失败)}erase(span); // 调用erase方法删除该节点return span; // 返回弹出的Span节点}// 插入操作:在指定位置pos之前插入新节点newspan(双向链表插入逻辑)void insert(Span* pos, Span* newspan){// 合法性检查:插入位置或新节点为空,输出日志并返回if(pos == nullptr || newspan == nullptr){Log(LogModule::DEBUG) << "nullptr can not insert!";return;}// 插入逻辑:调整指针,将newspan插入到pos的前驱节点与pos之间// 原关系:prev -> pos// 新关系:prev -> newspan -> posSpan* prev = pos->_prev; // 获取pos的前驱节点prev->_next = newspan; // 前驱节点的next指向新节点newspan->_prev = prev; // 新节点的prev指向前驱节点newspan->_next = pos; // 新节点的next指向pospos->_prev = newspan; // pos的prev指向新节点}// 删除操作:删除指定的Span节点target(双向链表删除逻辑)bool erase(Span* target) //TODO:待确认是否需要释放target的内存(当前仅从链表移除){// 合法性检查:目标节点为空,或目标节点的前后指针异常,输出日志并返回失败if(target == nullptr || target->_next == nullptr || target->_prev == nullptr){Log(LogModule::DEBUG) << "nullptr can not erase!";return false;}// 删除逻辑:调整目标节点前后节点的指针,断开目标节点与链表的连接// 原关系:prev -> target -> next// 新关系:prev -> nextSpan* prev = target->_prev; // 获取目标节点的前驱Span* next = target->_next; // 获取目标节点的后继prev->_next = next; // 前驱的next指向后继next->_prev = prev; // 后继的prev指向前驱// 注意:此处仅从链表移除target,未释放target的内存(需外部根据业务逻辑处理)return true; // 返回删除成功}// 判空操作:判断链表是否为空(头节点的next指针指向自身即为空)bool empty(){return _head->_next == _head;}// 析构函数:释放链表头节点的内存(避免内存泄漏)~SpanList(){delete _head;}private:// 链表头节点:双向循环链表的哨兵节点,不存储实际Span数据,仅用于简化链表操作Span* _head;public:// 互斥锁:保护链表的并发访问(多线程操作时需加锁,避免数据竞争)std::mutex _mutex;
};
Thread cache类
#pragma once
// 引入依赖头文件:日志、通用工具、自由链表、内存大小计算、中心缓存
#include "Log.hpp"
#include "common.h"
#include "FreeList.hpp"
#include "SizeClass.hpp"
#include "Centralcache.hpp"
#include <algorithm> // 用于 std::min 函数(计算批量申请数量)
#include <cstddef> // 用于 size_t 等基础类型// ThreadCache 类:继承 NonCopyable 禁止拷贝构造和赋值(确保每个线程唯一实例)
class ThreadCache : public NonCopyable
{
public:// 构造函数:默认空实现(成员 _freelists 会自动调用 FreeList 默认构造)ThreadCache(){}// 内存分配接口:对外提供的核心分配函数,输入需求内存大小,返回分配的内存指针void* Allocate(size_t size){void* obj = nullptr; // 存储最终分配的内存块指针,初始化为空// 1. 合法性检查:排除无效大小(≤0 或超过 ThreadCache 处理上限 MAX_BYTES)if(size <= 0 || size > MAX_BYTES){Log(WARNING) << "size invalid!"; // 日志输出警告return obj; // 返回空指针(分配失败)}// 2. 内存大小对齐:将需求大小向上对齐到预设的标准大小(避免内存碎片)int alignsize = SizeClass::RoundUp(size);// 3. 计算哈希索引:根据对齐后的大小,找到对应的自由链表在 _freelists 中的位置int index = SizeClass::Index(size);// 4. 优先从本地自由链表分配:若对应链表非空,直接弹出一个内存块if(!_freelists[index].Empty()){obj = _freelists[index].Pop();}// 5. 本地缓存不足:向 CentralCache 批量申请内存块else{obj = FetchFromCentralCache(index, alignsize);}return obj; // 返回分配的内存块(成功非空,失败为空)}// 内存释放接口:对外提供的核心释放函数,输入待释放内存指针和其原始大小void Deallocate(void* ptr, size_t size){// 1. 合法性检查:排除无效大小、空指针if(size <= 0 || size > MAX_BYTES || ptr == nullptr){Log(WARNING) << "size invalid or ptr is null!"; // 日志输出警告return; // 直接返回(释放失败)}// 2. 计算哈希索引:根据原始大小找到对应的自由链表位置(无需对齐,因分配时已对齐)int index = SizeClass::Index(size);// 3. 将内存块归还给本地自由链表(线程内操作,无锁,效率高)_freelists[index].Push(ptr);// TODO:待实现逻辑——当自由链表长度超过阈值时,将多余内存块归还给 CentralCache(避免本地缓存囤积)}// 核心辅助函数:向 CentralCache 批量申请内存块,补充本地缓存void* FetchFromCentralCache(size_t index, size_t size){// 慢开始反馈调节算法:动态控制向 CentralCache 申请的批量大小,平衡效率与内存利用率// 核心逻辑:// 1. 初始申请量小,避免内存浪费;// 2. 若该大小内存持续需求,申请量逐步增长(直至上限);// 3. 内存块越大,单次申请量越小(大内存浪费风险高);// 4. 内存块越小,单次申请量越大(小内存分配频繁,减少申请次数)// 计算本次申请量:取「本地链表最大限制」与「SizeClass 推荐量」的较小值size_t batchNum = std::min(_freelists[index].Maxsize(), SizeClass::NumMoveSize(size));// 若本次申请量达到本地链表的当前最大限制,下次允许申请更多(动态增长)if (batchNum == _freelists[index].Maxsize()) {_freelists[index].AddMaxsize(1); // 最大限制 +1(后续申请量可更大)}// 1. 获取 CentralCache 单例实例(全局唯一)CentralCache* ccins = CentralCache::GetInstance();void* start = nullptr; // 存储从 CentralCache 获取的内存块链表头void* end = nullptr; // 存储从 CentralCache 获取的内存块链表尾// 2. 向 CentralCache 批量申请内存块:传入申请量、内存大小,返回实际获取的数量size_t actualNum = ccins->FetchRangeObj(start, end, batchNum, size);// 3. 处理申请结果:实际获取数量无效(<1),日志警告并返回空if(actualNum < 1){Log(LogModule::DEBUG) << "actualnum invalid";return nullptr;}// 4. 仅获取到 1 个内存块:直接返回给调用者(无需加入本地链表,减少操作)else if(actualNum == 1){return start;}// 5. 获取到多个内存块:将「第一个块之外的所有块」加入本地自由链表(第一个块返回给调用者)// NextObj(start):获取 start 内存块的下一个块(依赖内存块头部的 next 指针)_freelists[index].PushRange(NextObj(start), end);return start; // 返回第一个内存块给调用者}// 待实现函数:处理「本地自由链表过长」的逻辑(将多余块归还给 CentralCache)void ListTooLong(FreeList& list, size_t size){// TODO:后续需实现——例如取出链表中一半的块,调用 CentralCache 接口归还给中心缓存}// 析构函数:默认空实现(若后续需在线程退出时释放本地缓存,需补充逻辑)~ThreadCache(){}private:// 核心成员:哈希桶结构的自由链表数组,每个桶对应一类对齐后的内存大小// NFREELISTS:自由链表数量(与 SizeClass 中对齐大小的类别数量一致)FreeList _freelists[NFREELISTS];
};/*** 线程局部的 ThreadCache 单例指针:高性能内存分配器的核心设计* 关键修饰符说明:* 1. static:静态存储期,指针本身在程序整个生命周期内存在(不会栈销毁)* 2. thread_local:线程局部存储,每个线程拥有该指针的独立副本(线程间互不干扰)* 3. ThreadCache*:指针类型,指向当前线程的 ThreadCache 实例* 4. = nullptr:初始化为空指针(需在首次使用时创建 ThreadCache 实例)* 作用:确保每个线程操作自己的本地缓存,完全避免线程间锁竞争,最大化分配效率
*/
static thread_local ThreadCache* pTLSThreadCache = nullptr;
Central cache类
#pragma once
// 引入依赖头文件:日志工具、内存大小管理、通用配置、Span链表管理、标准互斥锁
#include "Log.hpp"
#include "SizeClass.hpp"
#include "common.h"
#include "Spanlist.hpp"
#include <cstddef>
#include <mutex>// CentralCache 类:中心缓存,继承 NonCopyable 禁止拷贝(确保全局唯一实例)
class CentralCache : NonCopyable
{
private:// 私有构造函数:禁止外部直接实例化(单例模式核心,确保仅通过 GetInstance 创建)CentralCache(){}public:// 单例模式接口:获取 CentralCache 全局唯一实例(双检锁 DCLP,保证线程安全)static CentralCache* GetInstance(){static CentralCache* instance = nullptr; // 静态指针存储单例实例// 第一次检查:避免每次调用都加锁(提升效率)if(instance == nullptr){// 加锁:确保多线程下仅初始化一次std::lock_guard<std::mutex> lock(_mutex);// 第二次检查:防止多线程并发时重复初始化if(instance == nullptr){instance = new CentralCache; // 初始化单例实例}}return instance; // 返回单例实例}// 核心辅助方法:从指定的 SpanList 中获取一个有空闲内存块的 Span// 参数:list - 待查找的 Span 链表;byte_size - 需要的内存块大小(对齐后)// 返回值:有空闲块的 Span(未实现,当前返回 nullptr)Span* GetOneSpan(SpanList& list, size_t byte_size){// TODO:待实现核心逻辑// 1. 遍历 list,查找 _freelist 非空的 Span(有空闲内存块)// 2. 若未找到,向 PageCache 申请新的 Span(按 byte_size 计算所需页数)// 3. 将新 Span 切割为 byte_size 大小的小内存块,串联成 _freelist// 4. 将新 Span 加入 list,返回该 Spanreturn nullptr;}// 核心对外接口:向 ThreadCache 批量提供内存块// 参数:// start/end - 输出参数,存储获取到的内存块链表的头/尾指针// batchNum - ThreadCache 期望申请的批量数量// size - 单个内存块的大小(对齐后)// 返回值:实际获取到的内存块数量(0 表示失败)size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size){// 1. 计算 size 对应的哈希桶索引(与 ThreadCache/SizeClass 规则一致)size_t index = SizeClass::Index(size);// 2. 加桶锁:仅锁定当前哈希桶的 SpanList,减少多线程竞争(提升并发效率)std::lock_guard<std::mutex> lock(_spanlist[index]._mutex);// 3. 获取一个有空闲块的 Span(调用 GetOneSpan,未实现时返回 nullptr)Span* span = GetOneSpan(_spanlist[index], size);// 4. 检查 Span 有效性:Span 为空或其自由链表为空,日志警告并返回 0if(span == nullptr || span->_freelist == nullptr){Log(LogModule::DEBUG) << "GetOneSpan fail!";return 0;}// 5. 初始化内存块链表:从 Span 的 _freelist 开始取块size_t actualNum = 1; // 实际获取的数量(至少 1 个,初始化为 1)start = span->_freelist; // 链表头指向 Span 自由链表的第一个块end = start; // 链表尾初始与头重合(仅 1 个块时)size_t i = 0;// 6. 批量取块:最多取 batchNum 个,或取完 Span 中所有空闲块(有多少拿多少)// 循环条件:未取满 batchNum-1 个(因已初始化 1 个)、当前块非空、下一个块非空while (i < batchNum - 1 && end != nullptr && NextObj(end) != nullptr) {end = NextObj(end); // 尾指针后移到下一个块i++; // 计数+1actualNum++; // 实际数量+1span->_usecount++; // Span 的已使用块计数+1(跟踪使用状态)}// 7. 更新 Span 的自由链表:指向剩余未取走的第一个块(若有)span->_freelist = NextObj(end);// 8. 切断内存块链表:将取出的最后一个块的 next 置空(避免与 Span 剩余块关联)NextObj(end) = nullptr;// 9. 返回实际获取的内存块数量return actualNum;}// 析构函数:默认空实现(单例实例通常随程序生命周期结束,无需手动释放)~CentralCache(){}private:// 核心成员:哈希桶结构的 SpanList 数组,每个桶对应一类对齐后的内存块大小// NFREELISTS:桶数量(与 SizeClass 中内存块类别数量一致)SpanList _spanlist[NFREELISTS];// 静态互斥锁:保护单例实例的初始化过程(双检锁中的锁)static std::mutex _mutex;
};