当前位置: 首页 > news >正文

【从零实现高并发内存池】Central Cache从理解设计到全面实现

📢博客主页:https://blog.csdn.net/2301_779549673
📢博客仓库:https://gitee.com/JohnKingW/linux_test/tree/master/lesson
📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!
📢本文由 JohnKi 原创,首发于 CSDN🙉
📢未来很长,值得我们全力奔赴更美好的生活✨

在这里插入图片描述

在这里插入图片描述

文章目录

  • 🏳️‍🌈一、central cache
    • 1.1 整体设计
    • 1.2 不同之处
    • 1.3 结构设计
      • 1.3.1 Span 类
      • 1.3.2 SpanList 类
    • 1.4 Central Cache 类
      • 1.4.1 单例模式
      • 1.4.2 慢开始反馈调节算法
      • 1.4.3 从中心缓存获取对象
  • 👥总结


🏳️‍🌈一、central cache

1.1 整体设计

当线程申请某一大小的内存时,如果thread cache中对应的自由链表不为空,那么直接取出一个内存块进行返回即可,但如果此时该自由链表为空,那么这时thread cache就需要向central cache申请内存了。
./,

central cache 的结构与 thread cache 是相同的,都是哈希桶结构,并且它们遵循同样的的哈希桶对齐映射规则。这样的好处是: 当thread cache的某个桶中没有内存了,就可以直接到central cache申请内存。

1.2 不同之处

thread cachecentral cache 有两个明显不同的地方。

  1. thread cache是每个线程独享的,而central cache是所有线程共享的 ,因为每个线程的thread cache没有内存了都会去找central cache,因此 在访问central cache时是需要加锁的。

  2. central cache在加锁时并不是将整个central cache全部锁上了,central cache在加锁时用的是桶锁,也就是说每个桶都有一个锁 ,也就是说每个桶都有一个锁。此时 只有当多个线程同时访问central cache的同一个桶时才会存在锁竞争,如果是多个线程同时访问central cache的不同桶就不会存在锁竞争。

  3. thread cache的每个桶中挂的是一个个切好的内存块而central cache的每个桶中挂的是一个个的span ,每个映射桶下面的span中的大内存块被按映射关系切成了一个个小内存块对象挂在span的自由链表中。

在这里插入图片描述

1.3 结构设计

1.3.1 Span 类

在内存池设计中,Span 是 ​管理大块内存的核心数据结构它连接了中心缓存(Central Cache)与页堆(Page Heap),承担着内存块的分割、分配与回收的核心职责。

每个span管理的都是一个以页为单位的大块内存,每个桶里面的若干span是按照双链表的形式链接起来的,并且每个span里面还有一个自由链表,这个自由链表里面挂的就是一个个切好了的内存块,根据其所在的哈希桶这些内存块被切成了对应的大小。

但是,这样也会有一个难题,就是 页号的类型应该设置成什么呢?

每个程序运行起来后都有自己的进程地址空间,在32位平台下,进程地址空间的大小是232 ;而在64位平台下,进程地址空间的大小就是264

在这里插入图片描述
页大小:操作系统默认页大小通常为 ​4KB​(可配置为 2MB/1GB 大页)。

页号计算公式​:

PAGE_ID = Physical_Address >> Page_Shift;

例如:物理地址 0x12345000,页大小为 4KB(Page_Shift=12),则页号 _PAGEID = 0x12345

若物理地址超过 32 位范围(如 0x2000000000),X86 的 uint32_t 会溢出,导致页号计算错误。

X86:32 位页号足够处理 4GB 内存的连续性检测。
​X64:必须使用 64 位页号,否则大内存场景(如 128GB)会出现错误合并或无法合并。

所以我们可以通过 预编译指令 动态选择数据类型

#ifdef _WIN64typedef unsigned long long PAGE_ID;
#elif _WIN32typedef size_t PAGE_ID;
#endif

这里需要注意的是 先判断是否是 _WIN64,在判断是否是 _WIN32
原因在于

  • 在 64 位 Windows 环境下,会先检查 _WIN32,由于 _WIN32 总是被定义,条件 #ifdef _WIN32 会直接成立

Span 类结构

struct Span {PAGE_ID _PAGEID = 0;	// 大块内存起始页的页号size_t _n = 0;			// 页的数量Span* _next = nullptr;	// 双向链表的结构Span* _prev = nullptr; size_t _userCount = 0;	// 切好小块内存,被分配给 thread cache 的计数void* _freeList = nullptr; // 切好小块内存的自由链表
};

_PAGEID标识内存块在操作系统分页系统中的起始位置。
例如:_PAGEID=100 表示该内存块从第100页开始(每页大小通常为4KB或8KB)。

_n表示该内存块包含的连续页数,用于计算总大小(总大小 = _n * 页大小)。

_next 与 _prev双向链表结构。将多个 Span 组织成 ​双向链表,用于中心缓存(Central Cache)管理不同大小的内存块。
例如:Central Cache 中可能存在多个链表,每个链表管理相同页数(_n)的 Span。

_userCount跟踪当前 Span 中有多少个小块内存被分配给线程缓存(Thread Cache)。
当 _userCount == 0 时,表示所有内存已归还,Span 可被回收至页堆。

_freeList维护未分配的小块内存链表,供 Thread Cache 快速分配。
链表节点通过 NextObj(obj) 宏(即 (void*)obj)连接

1.3.2 SpanList 类

我们已经说了 每个Span都是一个双向链表,所以我们需要对他进行一个封装。

这个类成员需要有一个头节点(哨兵位),一个桶锁并有增删、判空等功能

构造函数

构造函数创建头结点,并设置成双向循环链表结构。

		SpanList() {_head = new Span;_head->_next = _head;_head->_prev = _head;}

插入

		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;}

删除

注意:从双链表删除的span会还给下一层的page cache,相当于只是把这个span从双链表中移除,因此不需要对删除的span进行delete操作。

		void Erase(Span* pos) {assert(pos);// 不能删除哨兵位assert(pos != _head);// 删除 pos 结点Span* prev = pos->_prev;Span* next = pos->_next;prev->_next = next;next->_prev = prev;}

1.4 Central Cache 类

central cache 的映射规则和 thread cache 是一样的,因此central cache里面哈希桶的个数也是208但central cache每个哈希桶中存储就是我们上面定义的双链表结构。

1.4.1 单例模式

1、为了保证 CentralCache类 只能创建一个对象,我们需要将 central cache 的 构造函数 和 拷贝构造函数 设置为私有(C++98方法),或者在C++11中也可以在函数声明的后面加上=delete进行修饰。

2、CentralCache类当中还需要有一个CentralCache类型的静态的成员变量,程序运行起来后我们就立马创建该对象(类外创建),在此后的程序中就只有这一个单例了。

// 单例模式
class CentralCache {public:static CentralCache* GetInstance() {return &_sInst;}private:SpanList _spanLists[NFREELIST];private:// C++98,构造函数私有CentralCache(){}// C++11,禁止拷贝CentralCache(const CentralCache&) = delete;static CentralCache _sInst;
};

1.4.2 慢开始反馈调节算法

thread cachecentral cache 申请内存时,central cache应该给出多少个对象呢?
如果central cache给的太少,那么thread cache在短时间内用完了又会来申请;但如果一次性给的太多了,可能thread cache用不完也就浪费了。

根据上面的分析,我们在这里采用一个慢开始反馈调节算法。当thread cache向central cache申请内存时,如果申请的是较小的对象,那么可以多给一点,但是如果申请的是较大的对象,就可以少给一点(类似网络tcp协议拥塞控制的慢开始算法)。

慢开始反馈调节算法

  1. 最开始不会一次向 central cache 一次批量要太多,因为要太多了可能用不完
  2. 如果你不要这个 size 大小内存需求,那么 batchNum 就会不断增长,知道上线
  3. size 也越多,一次向 central cache 要的内存就越少 512
  4. size 也越少,一次向 central cache 要的内存就越多 2
		// 一次 thread cache 从中心缓存中获取多少个static size_t NumMoveSize(size_t size) {assert(size > 0);// [2, 512], 一次批量多少个对象的上下限值int num = MAX_BYTES / size;if(num < 2) num = 2;if(num > 512) num = 512;return num;}

注意:该函数封装在SizeClass类中,且为静态成员函数,无需创建对象调用。

上面的函数确实可以让申请对象合理化,但是如果申请的是小对象,一次性给出512个也是比较多的,基于这个原因,我们可以在FreeList这个类中增加一个_maxSize的成员变量,该变量初始值设置为1,并且提供一个公有成员函数用于获取这个变量 (返回值需要使用引用,后序需要修改)。也就是说thread cache的每个自由链表都有自己的_maxSize。

class FreeList {public:// ...private:void* _freeList = nullptr;size_t _maxSize = 1;
};

1.4.3 从中心缓存获取对象

每次 thread cachecentral cache 申请对象时,我们先通过慢开始反馈调节算法计算本次应该申请的对象个数,然后再向central cache进行申请。

void* ThreadCache::FetchFromCentralCache(size_t index, size_t size) {// 慢开始反馈调节算法// 1. 最开始不会一次向 central cache 一次批量要太多,因为要太多了可能用不完// 2. 如果你不要这个 size 大小内存需求,那么 batchNum 就会不断增长,知道上线// 3. size 也越多,一次向 central cache 要的内存就越少 512// 4. size 也越少,一次向 central cache 要的内存就越多 2size_t batchNum = std::min(_freeLists[index].MaxSize(), SizeClass::NumMoveSize(size));if (_freeLists[index].MaxSize() == batchNum) {_freeLists[index].MaxSize() += 1;}void* start = nullptr;void* end = nullptr;size_t actualNum = CentralCache::GetInstance()->FetchRangeObj(start, end, batchNum, size);assert(actualNum >= 1);// 申请到对象的个数是一个,则直接将这一个对象返回即可if (actualNum == 1) {assert(start == end);return start;}// 申请到对象的个数是多个,还需要将剩下的对象挂到thread cache中对应的哈希桶else {_freeLists[index].PushRange(NextObj(start), end);return start;}return nullptr;
}

该函数要从central cache获取n个指定大小的对象,这些对象肯定是从central cache对应的哈希桶的某个span中取出来的,因此取出来的n个对象是链接在一起的,我们只需要得到这段自由链表的头和尾即可,此处采用输出型参数进行获取。

// 从中心缓冲获取一定数量的对象给 thread cache
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size) {size_t index = SizeClass::Index(size);_spanLists[index]._mtx.lock();Span* span = GetOneSpan(_spanLists[index], size);assert(span);assert(span->_freeList);// 从 span 中获取 batchNum个对象// 如果不够 batchNum 个,有多少拿多少start = span->_freeList;end = start;size_t i = 0;size_t actualNum = 1;while (i < batchNum - 1 && NextObj(end) != nullptr) {end = NextObj(end);++i;++actualNum;}span->_freeList = NextObj(end);NextObj(end) = nullptr;span->_userCount += actualNum;_spanLists[index]._mtx.unlock();return actualNum;
}  

由于central cache是所有线程共享的,所以我们在访问central cache中的哈希桶时,需要先给对应的哈希桶加上桶锁,在获取到对象后再将桶锁解除。


👥总结

本篇博文对 【从零实现高并发内存池】Central Cache从理解设计到全面实现 做了一个较为详细的介绍,不知道对你有没有帮助呢

觉得博主写得还不错的三连支持下吧!会继续努力的~

相关文章:

  • 人工智能应用开发中常见的 工具、框架、平台 的分类、详细介绍及对比
  • 大象机器人推出myCobot 280 RDK X5,携手地瓜机器人共建智能教育机
  • 2025年最新总结安全基础(面试题)
  • MySQL 缓存机制全解析:从磁盘 I/O 到性能优化
  • Vue 图标动态加载:Ant Design Vue 的 a-tree 图标实现与优化
  • 人工智能(机器人)通识实验室解决方案
  • vue3环境搭建、nodejs22.x安装、yarn 1全局安装、npm切换yarn 1、yarn 1 切换npm
  • 21.C++11
  • UWB定位技术面临的主要挑战
  • Anconda环境下修改Jupyter notebook的启动路径(Windows)
  • 人工智能应用开发的四种主流方法(提示工程、大模型微调、RAG、Agent)的详细对比分析
  • Jenkins插件下载慢解决办法
  • 超细的ollama下载以及本地部署deepseek项目
  • 【第三章】18-常用模块6-ngx_http_mirr_module
  • CExercise_13_1排序算法_2归并排序
  • 基于EasyX库开发的球球大作战游戏
  • 银河麒麟(Kylin) - V10 SP1桌面操作系统ARM64编译QT-5.15.12版本
  • 2025年国企社招欧治链金再生资源入职测评笔试中智赛码平台SHL测试平台Verify认知能力测试
  • linux-设置每次ssh登录服务器的时候提醒多久需要修改密码
  • MCP调用示例-GitHub仓库操作
  • 国家发改委:是否进口美国饲料粮、油料不会影响我国粮食供应
  • 黄永年:说狄仁杰的奏毁淫祠
  • 北上广深还是小城之春?“五一”想好去哪玩了吗
  • 我国首个核电工业操作系统发布,将在华龙一号新机组全面应用
  • 男子称喝中药治肺结节三个月后反变大增多,自贡卫健委回应
  • 央行25日开展6000亿元MLF操作,期限为1年期