高并发内存池(二):Central Cache的实现
前言:本文将要讲解的高并发内存池,它的原型是Google的⼀个开源项⽬tcmalloc,全称Thread-Caching Malloc,近一个月我将以学习为目的来模拟实现一个精简版的高并发内存池,并对核心技术分块进行精细剖析,分享在专栏《高并发内存池》里,期待小伙伴们的热情支持与关注!
项目专栏:高并发内存池_敲上瘾的博客-CSDN博客
目录
一、CentralCache结构
二、CentralCache与ThreadCache的区别
三、CentralCache如何与ThreadCache联动
1.申请过程
2.释放过程
四、span结构与span链
五、CentralCache类
六、CentralCache类方法
七、源码
上期讲了Thread Cache的实现,但并未对在Central Cache中如何申请内存进行讲解。接下来让我们会对Central Cache的结构和如何在Central Cache中申请内存进行学习和代码实现。
一、CentralCache结构
Central Cache的结构和Thread Cache类似,同样是用一个哈希桶来管理内存,只不过更复杂了些。这种类似的结构也使得在Thread Cache层封装的各种处理方法可以直接拿过来用,方便了很多。
Central Cache的内存管理图如下:

这是一个哈希桶,里面储存的是一个span类型双向链表,span这个单词的意思是:跨度、宽度、跨距...... span是以页为单位的内存,里面又被分割成了多个小的内存块,即自由链表。然后又根据不同的对齐规则得到多个不同span双向链表组成一个桶。
当然一个span里面不单只有自由链表,里面还有各种管理信息,在下文会细讲。
二、CentralCache与ThreadCache的区别
ThreadCache在每个线程内各自有一份,不存在锁的竞争。
CentralCache是所有线程共用唯一一份,它是临界资源,需要加锁,但它不是直接就给哈希桶加一把大锁,而是在哈希桶内每一个span链有独立的锁,把它称为桶锁,所以锁竞争并不激烈。也就是只有当两个线程同类型内存块用完了,并且同时申请span桶,并映射到同一个span链上才会有锁竞争,所以触发概率非常小,效率依旧很高。
三、CentralCache如何与ThreadCache联动
1.申请过程
ThreadCache内自由链表桶没有相应的内存了,会到span桶中找,通过映射关系找到对应的span链,而在span链中只在一个span块中找内存,自由链表桶向span桶申请内存实际上是只要一块的,但是这样一个一个的给效率未免有些太低。就需要多给几块,那该怎么分呢?下文会解答。
当span链中没内存了再向下一层Page Cache申请。
2.释放过程
有时某些线程(线程1)可能需要大量的内存,不断地向span桶中申请。而用完后就被放回它自己的自由链表桶了,后面有线程再需要向span桶申请内存就没得了,大部分内存都堆积在线程1自由链表桶,而线程1又暂时用不了这么多,这个时候就需要还给span桶,来提供给其他线程使用。
因此在span链中,每个span里面的自由链表的节点个数是随机的,并不是第一个span的自由链表空间就是满,也可能是nullptr。
CentralCache在等线程把拿去用的空间全部还回来后,交个下一层PageCache进行整合,缓解内存碎片问题。这也是把span链做成双链表的一个原因,方便任意的span断开给PageCache。而span是怎么知道内存是否被全部还回来呢?其实在span内通过一个_useCount计数器来判断,在下文会提到。
所以CentralCache就起到一个承上启下均衡调度的作用。
四、span结构与span链
span链表的创建和自由链表一样我们放在common.h这个公共的文件里,因为span的内存单位是页,在span里面需要储存起始页的地址,那么这个地址要用什么类型来存是个问题。
地址实际上就是一个数字,假设以8kb为一页,在32位机器上的最大编址就是2^32/8kb = 2^19,如果是64位机器的话,最大的编址是2^64/8kb = 2^51,所以我们通过条件编译来做不同的储存方案。如下:
#ifdef _WIN64typedef unsigned long long PANGE_ID;
#elif _WIN32typedef size_t PANGE_ID;
#else//linux
#endif
接下来就只用拿着PANGE_ID这个类型去储存页号就行。
注意:32位机器有_WIN32的定义,但64位机器既有_WIN64的定义又有_WIN32的定义,所以_WIN64的判断必须放在第一位。
Span结构如下:
struct Span
{PANGE_ID _pageId = 0;//⼤块内存起始⻚的⻚号size_t _n = 0;//页的数量struct Span* _prev = nullptr;struct Span* _next = nullptr;size_t _useCount = 0;void* _freeList = nullptr;//... ...
};
- _pageId:内存起始⻚的⻚号
- _n:页的数量。
- _prev,_next:分别为前驱和后驱指针。
- _useCount:记录有多少内存块被使用,当有内存块被使用_useCount++,内存块还回来则_useCount--,所以就可以知道span中的内存是否被全部还回来了。
- _freeList:储存自由链表。
其余的成员函数在本章暂时用不到,就不列出来了。
然后封装一个类,用来做span链表的头,和提供一些操作方法:
class SpanList
{
public:SpanList(){}void Insert(Span* pos, Span* newNode);void Erase(Span* node);
private:Span* _head;
public:std::mutex _mtx;
};
- void Insert(Span* pos, Span* newNode):在pos前面插入newNode。
- void Erase():删除节点node。
- Span* _head:链表头节点指针。
- std::mutex _mtx:该链表的锁(桶锁)。
这个是一个双向链表所以在构造函数时就需要申请节点空间,并把指针头尾做连接,如下:
SpanList()
{_head = new Span;_head->_next = _head;_head->_prev = _head;
}
剩下的两个接口实现就比较基础, 在这里就不再讲解,文末会给出源码。
五、CentralCache类
CentralCache类核心就是一个span链表桶,因为所有线程共用一份,所以把它作为单例模式(饿汉模式)防止被创建多份,创建在CentralCache.h文件里。
单例模式需要注意的事项:
- 把构造函数设为私有,不让它默认生成拷贝构造函数,拷贝赋值函数。
- 声明为静态全局变量,最好在.cpp文件里定义。
- 需要定义一个获取单例对象的方法。
class CentralCache
{
private:CentralCache() {}CentralCache(const CentralCache&) = delete;CentralCache& operator=(const CentralCache&) = delete;
public:static CentralCache* GetInstance();//......
private:SpanList _spanLists[FREE_LIST_SIZE];//span桶static CentralCache _sInst;//CentralCache对象声明
};
- GetInstance():获取CentralCache单例对象。
其它类方法在这里先不设,要不然挺突兀的,在下文分析如何申请内存时再根据需要添加。
六、CentralCache类方法
在上篇文章讲到如果ThreadCache中内存不够,需要到CentralCache中申请,大家简单回顾一下,文章链接:高并发内存池(一):项目介绍和Thread Cache实现-CSDN博客

接下来讲的所有内容都是对FetchFromCentralCache接口进行实现。
申请多少空间的问题
上文提到一次不能就只给一块空间,这样效率太低,而给多浪费,给少效率低,该怎么平衡呢?可以做这样的算法处理:
说明:
- size:对齐后用户申请空间的大小。
- batchNum:存申请的内存块个数。
慢开始反馈调节算法:
- size越大,一次向central cache要的batchNum就越小。
- size越小,一次向central cache要的batchNum就越大。
- 最开始不会一次向central cache一次批量要太多,因为要太多了可能用不完。
- 如果不断申请size大小的内存需求,那么batchNum就会不断增长,直到上限。
static int const MAX_BYTES = 256 * 1024;
static inline size_t NumMoveSize(size_t size)
{int ret = MAX_BYTES / size;//把个数控制在[2,512]之间if (ret < 2) ret = 2;if (ret > 512) ret = 512;return ret;
}
- MAX_BYTES / size:可以让小块内存申请得多一些,大块内存申请得少一些。
- if (ret < 2) ret = 2:申请太少了不足2就补充到2。
- if (ret > 512) ret = 512:申请太多了超过512就截断到512。
因为是对申请内存相关的计算所以把这个函数放到common.h的SizeClass类里,而MAX_BYTES定义放在文件开头更为合理。
以上满足了1,2点要求,针对3,4点,在ThreadCache的自由链表桶里添加成员变量来记录这次该申请多少,然后这个计数会根据申请的次数增加而增加。

注意:为了在外部对_maxSize修改,这个把返回值设为左值引用。
int batchNum = std::min(SizeClass::NumMoveSize(size), _freelists[index].MaxSize());
if (batchNum == _freelists[index].MaxSize())_freelists[index].MaxSize() += 1;
取两种方案得到的最小值,如果这个最小值是_maxSize则_maxSize增大,增大的幅度可以是1,2,3等等,这里就设为1。
内存申请
接下来就是去CentralCache里取batchNum个大小为size的内存,实际上就是取对应的span块中的一小段自由链表,用什么来存储呢?
因为是一段自由链表,就需要获得它的头尾,所以用这样两个变量:
- void* start = nullptr:指向自由链表的头。
- void* end = nullptr:指向自由链表的尾部。
先把代码实现给出,然后再来解释:
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{//计算batchNum大小和一些准备工作int batchNum = std::min(SizeClass::NumMoveSize(size), _freelists[index].MaxSize());if (batchNum == _freelists[index].MaxSize())_freelists[index].MaxSize() += 1;void* start = nullptr;void* end = nullptr;//向CentralCache申请内存int actualNum = CentralCache::GetInstance()->FetchRangeObj(start, end, batchNum, size);assert(actualNum > 1);if (actualNum == 1){assert(start == end);return start;}else{//插入到线程自己的自由链表桶中,因为start将要被使用所以不用插。_freelists[index].PushRange(Nextobj(start), end);return start;}
}
内存申请是在span桶内完成的所以在CentralCache中做一个类方法FetchRangeObj,这个函数我们待会来实现,现在先把当前函数逻辑走完。
注意:我们最终只在一个span节点上申请内存,所以span中自由链表节点个数可能不够batchNum个,不过没关系,我们把实际申请到的节点个数返回,这里使用actualNum记录。
现在申请到内存了,需要把它连接到线程自己的自由链表里面,但需要分两种情况:
- 只申请到一个,因为当前用户就需要一个所以不用连接到自由链表,直接返回给用户使用。
- 申请到多个,其中有一个要返回给用户使用,把start的下一个到end这一段连接到线程到自由链表。
这里封装了Nextobj接口用来取到自由链表的下一个节点,实现很简单如下:
static void*& Nextobj(void* obj)
{return *(void**)obj;
}
注意:把它做成静态函数放在common.h文件里供全局使用,返回值类型设为一个引用,这样就可以对节点进行修改。
然后在自由链表里面封装一个方法PushRange,用来实现插入一段自由链表,比较简单直接把它插到头节点后面就行,这里就不在展示,文末会附有源码。
FetchRangeObj的实现
FetchRangeObj是CentralCache的类方法,我们再做一个对应的源文件CentralCache.cpp,然后记得把CentralCache(单例模式)的定义放在这个.cpp文件里面。
在FetchRangeObj首先调用类方法得到span链表桶的下标从而确定相应的span链表,然后上锁。
此时我们只是找到了span链表,然后需要在span链表里找到一个span节点,找span节点这个过程我们再单独封装一个方法GetOneSpan,同样放CentralCache类里面,因为它比较复杂,如果没有可以用的span节点还需要向下一层PageCache申请,该函数在本文不会实现,请期待下一篇博客的讲解!
现在假设我们在GetOneSpan内已经申请到一个span节点,接下来去取内存,让start和end都指向span的自由链表头,然后end往后移并做计数,但是需要注意几个问题:
- 内存可能不够batchNum个,那么有多少取多少。
- 当内存不够时,end会指向空,此时不能再被解引用。
- 取完内存后要对span中自由链表指向进行更新,并把end的next指向nullptr。
实现如下:
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{//... ...return nullptr;
}
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{//计算Span链表桶下标int index = SizeClass::RoundUp(size);//需要访问共享资源,加锁_spanLists[index]._mtx.lock();//获取一个Span节点Span* span = GetOneSpan(_spanLists[index], size);assert(span);assert(span->_freeList);//从Span节点中取到小块内存,如果足够batchNum个就取batchNum个,不足则全部取出。start = end = span->_freeList;int ret = 1;while (ret < batchNum && Nextobj(end) != nullptr){end = Nextobj(end);ret++;}span->_freeList = Nextobj(end);Nextobj(end) = nullptr;_spanLists[index]._mtx.unlock();return ret;
}
当取完内存后需要做善后处理:
- 更新span自由链表是指向,把它被取走的内存从链表中断开,即span->_freeList = Nextobj(end);
- 把end的下一个节点指向空,目的是和原span中自由链表断开。
- 解锁,返回实际申请的内存数量。
到现在为止CentralCache还没有完全结束,只实现了内存申请部分,还有内存释放的逻辑并未涉及,后续将会持续输出,家人们敬请期待!💕💕
非常感谢您能耐心读完这篇文章。倘若您从中有所收获,还望多多支持呀!
七、源码
代码量比较大,就不放在这里了,需要的小伙伴到我的gitee上取:
ConcurrentMemoryPool/ConcurrentMemoryPool · 敲上瘾/ConcurrentMemoryPool - 码云 - 开源中国
相关文章:
高并发内存池(二):Central Cache的实现
前言:本文将要讲解的高并发内存池,它的原型是Google的⼀个开源项⽬tcmalloc,全称Thread-Caching Malloc,近一个月我将以学习为目的来模拟实现一个精简版的高并发内存池,并对核心技术分块进行精细剖析,分享在…...
[Windows] VutronMusic v1.6.0 音乐播放器纯净版,可登录同步
VutronMusic-简易好看的PC音乐播放器 链接:https://pan.xunlei.com/s/VOMq7P_fTyhLUXeGerDVhrCTA1?pwduvut# VutronMusic v1.6.0 音乐播放器纯净版,可登录同步...
macvlan 和 ipvlan 实现原理及设计案例详解
一、macvlan 实现原理 1. 核心概念 macvlan 允许在单个物理网络接口上创建多个虚拟网络接口,每个虚拟接口拥有 独立的 MAC 地址 和 IP 地址。工作模式: bridge 模式(默认):虚拟接口之间可直接通信,类似交…...
【蓝桥杯】每日练习 Day19,20
目录 前言 蒙德里安的梦想 分析 最短Hamilton路径 分析 代码 乌龟棋 分析 代码 松散子序列 分析 代码 代码 前言 今天不讲数论(因为上课学数论真是太难了,只学了高斯消元)所以今天就不单独拿出来讲高斯消元了。今天讲一下昨天和…...
《AI大模型应知应会100篇》第7篇:Prompt Engineering基础:如何与大模型有效沟通
第7篇:Prompt Engineering基础:如何与大模型有效沟通 摘要 Prompt Engineering(提示工程)是与大模型高效沟通的关键技能。通过精心设计的Prompt,可以让模型生成更准确、更有用的结果。本文将从基础知识到高级策略&…...
微服务架构技术栈选型避坑指南:10大核心要素深度拆解
微服务架构的技术栈选型直接影响系统的稳定性、扩展性和可维护性。以下从10大核心要素出发,结合主流技术方案对比、兼容性评估、失败案例及优化策略,提供系统性选型指南。 1. 服务框架与通信 关键考量点 扩展性:框架需支持定制化扩展&#x…...
Elasticsearch 正排索引
一、正排索引基础概念 在 Elasticsearch 中,正排索引用于存储完整的文档内容,以便通过文档ID 快速定位文档的字段值。正排索引通过 Doc Values 和 Store Fields 两种形式,为聚合、排序、脚本计算等场景提供高效支持。Doc Values 的列式存储设…...
Spring实现WebScoket
SpringWeb编程方式分为Servlet模式和响应式。Servlet模式参考官方文档:Web on Servlet Stack :: Spring Framework,响应式(Reacive)参考官方文档:Web on Reactive Stack :: Spring Framework。 WebSocket也有两种编程方…...
Token是什么?
李升伟 整理 “Token” 是一个多义词,具体含义取决于上下文。以下是几种常见的解释: 1. 计算机科学中的 Token 定义:在编程和计算机科学中,Token 是源代码经过词法分析后生成的最小单位,通常用于编译器和解释器。 …...
odoo-045 ModuleNotFoundError: No module named ‘_sqlite3‘
文章目录 一、问题二、解决思路 一、问题 就是项目启动,本来好好地,忽然有一天报错,不知道什么原因。 背景: 我是在虚拟环境中使用的python3.7。 二、解决思路 虚拟环境和公共环境直接安装 sqlite3 都会报找不到这个库的问题…...
cesium加载CTB生成的地形数据
由于CTB生成的地形数据是压缩的(gzip)格式,需要在nginx加上特殊配置才可以正常加载,NGINX全部配置如下 worker_processes 1; events {worker_connections 1024; } http {include mime.types;default_type application/o…...
前端JS高阶技法:序列化、反序列化与多态融合实战
✨ 摘要 序列化与反序列化作为数据转换的核心能力,与多态这一灵活代码设计的核心理念,在现代前端开发中协同运作,提供了高效的数据通信与扩展性支持。 本文从理论到实践,系统解析: 序列化与反序列化的实现方式、使用…...
TS中的Class
基本用法 implements implements 关键字用于传递对类产生约束的数据类型 interface AnimalInfo{name:stringrace:stringage:number }interface AnimalCls{info:AnimalInfosayName():void} class Animal implements AnimalCls{info:AnimalInfoconstructor(info:AnimalInfo) {t…...
RustDesk 开源远程桌面软件 (支持多端) + 中继服务器伺服器搭建 ( docker版本 ) 安装教程
在需要控制和被控制的电脑上安装软件 github开源仓库地址 https://github.com/rustdesk/rustdesk/releases 蓝奏云盘备份 ( exe ) https://geek7.lanzouw.com/iPf592sadqrc 密码:4esi 中继服务器设置 使用docker安装 sudo docker image pull rustdesk/rustdesk-server sudo…...
【计网速通】计算机网络核心知识点与高频考点——数据链路层(二)
数据链路层核心知识点(二) 涵盖局域网、广域网、介质访问控制(MAC层)及数据链路层设备 上文链接:https://blog.csdn.net/weixin_73492487/article/details/146571476 一、局域网(LAN,Loacl Area Network&am…...
STM32单片机入门学习——第3-4节: [2-1、2]软件安装和新建工程
写这个文章是用来学习的,记录一下我的学习过程。希望我能一直坚持下去,我只是一个小白,只是想好好学习,我知道这会很难,但我还是想去做! 本文写于:2025.04.01 STM32开发板学习——第一节: [1-1]课程简介 前言开发板说明引用解答和…...
W3C XML Schema 活动
W3C XML Schema 活动 概述 W3C XML Schema(XML Schema)是万维网联盟(W3C)定义的一种数据描述语言,用于定义XML文档的结构和约束。XML Schema为XML文档提供了一种结构化的方式,确保数据的一致性和有效性。本文将详细介绍W3C XML Schema的活动,包括其发展历程、主要特点…...
爬虫【Scrapy-redis分布式爬虫】
Scrapy-redis分布式爬虫 1.Scrapy-redis实现增量爬虫 增量爬虫的含义 就是前面所说的的暂停、恢复爬取 安装 # 使用scrapy-redis之前最好将scrapy版本保持在2.8.0版本, 因为2.11.0版本有兼容性问题 pip install scrapy==2.8.0 pip install scrapy-redis -i https://pypi.tun…...
intellij Idea 和 dataGrip下载和安装教程
亲测有效 第一步:卸载老版本idea/Datagrip (没有安装过的可跳过此步骤) 第二步:下载idea/dataGrip安装包 建议选择2022以后的版本 官网: https://www.jetbrains.com/datagrip/download/other.html 选择dataGrip 的…...
轻量级搜索接口技术解析:快速实现关键词检索的Java/Python实践
Hi,你好! 轻量级搜索接口技术解析:快速实现关键词检索的Java/Python实践 接口特性与适用场景 本接口适用于需要快速集成搜索能力的开发场景,支持通过关键词获取结构化搜索结果。典型应用场景包括: 垂直领域信息检索…...
架构设计基础系列:事件溯源模式浅析
图片来源网络,侵权删 1. 引言 1.1 研究背景 传统CRUD模型的局限性:状态覆盖导致审计困难、无法追溯历史。分布式系统复杂性的提升:微服务架构下数据一致性、回滚与调试的需求激增。监管合规性要求:金融、医疗等领域对数…...
ResNet系列和ViT系列预训练模型权重文件下载
一、简单介绍 OpenAI CLIP项目提供的预训练模型权重文件列表,主要包含两种架构系列和不同规模配置: ResNet系列 (RN) 基础版本:RN50(ResNet-50)扩展版本:RN50x4、RN50x16、RN50x64(宽度扩展&am…...
【力扣hot100题】(035)二叉树的中序遍历
正常方法递归很简单,于是又学了一种栈的方法。 原理如下:每次循环先尽量将目前节点入栈并左移,没有左节点时回到栈首节点将目前节点放入结果容器中并移出栈外,目前节点变为该节点的右节点,循环结束条件是目前节点为nu…...
《数字图像处理》教材寻找合作者
Rafael Gonzalez和Richard Woods所著的《数字图像处理》关于滤波器的部分几乎全错,完全从零开始写,困难重重。关于他的问题已经描述在《数字图像处理(面向新工科的电工电子信息基础课程系列教材)》。 现寻找能够共同讨论、切磋、…...
批量删除 txt/html/json/xml/csv 等文本文件中的重复行
在文本文件中,可能会存在一些重复的数据行,这可能不是我们期望的,因此我们会碰到需要删除文本文件中重复行的情况。如果是人工删除,当文件较大或者数量较多的时候,处理的难度会较大。今天就给大家介绍一下批量删除文本…...
LangChain 使用向量数据库介绍与使用
LangChain 是一个用于构建大语言模型(LLM)应用的框架,而向量数据库在 LangChain 中主要用于实现检索增强生成(RAG, Retrieval-Augmented Generation),即通过向量搜索从外部知识库中快速检索相关信息,辅助大模型生成更准确的回答。以下是具体的使用方法: 1. 核心流程 L…...
基于微信小程序的智慧乡村旅游服务平台【附源码】
基于微信小程序的智慧乡村旅游服务平台(源码L文说明文档) 目录 4系统设计 4.1系统功能设计 4.2系统结构 4.3.数据库设计 4.3.1数据库实体 4.3.2数据库设计表 5系统详细实现 5.1 管理员模块的实现 5.1.1旅游景点管理…...
d202542
一、142.环形链表I 142. 环形链表 II - 力扣(LeetCode) 用set统计一下 如果再次出现那么就环的第一个return返回就行 public ListNode detectCycle(ListNode head) {Set<ListNode> set new HashSet<>();ListNode cur head;while(cur ! …...
NodeTextFileCollectorScrapeError 报警原因及解决方法
现象 prometheus 经常有告警 NodeTextFileCollectorScrapeError 查看 node-exporter 日志出现如下报错 time2025-04-01T06:43:18.266Z levelERROR sourcetextfile.go:248 msg"failed to collect textfile data" collectortextfile fileipmitool.prom err"fail…...
RapidJSON 处理 JSON(高性能 C++ 库)(四)
第四部分:RapidJSON 处理 JSON(高性能 C++ 库) 📢 快速掌握 JSON!文章 + 视频双管齐下 🚀 如果你觉得阅读文章太慢,或者更喜欢 边看边学 的方式,不妨直接观看我录制的 RapidJSON 课程视频!🎬 视频里会用更直观的方式讲解 RapidJSON 的核心概念、实战技巧,并配有…...
