【C++高并发内存池篇】ThreadCache 极速引擎:C++ 高并发内存池的纳秒级无锁革命!

【C++高并发内存池篇】ThreadCache 极速引擎:C++ 高并发内存池的纳秒级无锁革命!

📌本篇摘要

📝本篇为高并发内存池项目的正式开篇,通过把项目拆成一部分一部分去实现,这里会分成ThreadCacheCentralCachePageCache等三部分去分部实现内存池,这里就通过一步步分析来简单实现ThreadCache这步(可能会不完整,后续继续补充),欢迎阅读!

在这里插入图片描述


在这里插入图片描述
🏠欢迎拜访🏠:点击进入博主主页
📌本篇主题📌:ThreadCache框架部分简单实现
📅制作日期📅:2025.08.25
🧭隶属专栏🧭:
点击进入所属C++高并发内存池项目专栏

一· 💡concurrent memory pool整体轮廓分析

分为三部分:

  • 线程缓存(ThreadCache)每个线程独有,用于分配小于 256KB 的内存,申请内存无需加锁,独享特性使并发线程池高效。
  • 中心缓存(CentralCache)所有线程共享,ThreadCache 能按需从其获取对象,CentralCache也负责回收ThreadCache 对象以均衡内存分配,因存在竞争,取内存对象需加桶锁,但竞争不激烈。

页缓存(PageCache)位于 CentralCache 上层,以页为单位存储分配内存,CentralCache 无对象时从其分配 page 并切割给 CentralCache ,回收符合条件的 span 对象并合并相邻页缓解内存碎片问题 。

在这里插入图片描述

⚠️需注意问题

  • 性能问题。
  • 多线程环境下,锁竞争问题。
  • 内存碎⽚问题。

总之,从整体我们可以看出来,把整个项目的实现分成了三部分,而这三部分,它们两两之间都是有交互的,下面我们就从ThreadCache开始实现!

二· 🔍ThreadCache设计

🎆设计思想

  • threadcache是哈希桶结构,每个桶是⼀个按桶位置映射⼤⼩的内存块对象的⾃由链表。每个线程都会有⼀个threadcache对象,这样每个线程在这⾥获取对象和释放对象时是⽆锁的
在这里插入图片描述

这里拥有对应大小的内存块都是有一定规律的,按照对应对齐数排布的(后面会说到的),这样虽然缓解了外碎片问题,但避免不了出现内碎片的问题

目前,我们设计的这个简单的ThreadCache功能就分为两步:申请与释放内存(后续需要再进行补充)。

  • 申请内存:申请 ≤256KB 的内存时,先找线程本地的 threadcache,按内存大小算出自由链表下标 i;要是自由链表 _freeLists[i] 里有内存对象,直接拿一个出来用;要是 _freeLists[i] 里没对象,就从 central cache 批量拿一些,放进链表里,再拿一个出来用。
  • 释放内存:要释放小于 256k 的内存时,把它放回 thread cache,按大小算出自由链表桶位置 i,把对象推进 _freeLists[i]要是链表太长了,就回收一部分内存对象到 central cache

📝 代码设计

  • 首先我们还是先描述后组织发现可以把这个threadcache拆成几个部分:一个就是放不同块大小的链表freelist;一个就是这些链表的集合;因此我们先来实现freelist;之后再把这些链表组织起来就成了threadcache了。

✅ FreeList设计

  • 这里只需要支持我们删除(拿到对应内存块)以及插入(还回对应内存块)即可,用到的是我们之前定长内存池设计的去前指针大小字节保存下一个地址的方式进行的;而这个内存块链表不仅threadcache这里用到后面Centralcache也会用到,因此我们把它放到common公共类里面方便使用!

🛠️内存块链表整体代码实现:

classFreeList{public:// FreeList(){}// //这里不给它写构造,否则因为多个freelist的话初始化就会多次调用,然后没给_freelist初始化,就走缺省参数变成nullptr//然后就需要重复FREELIST_NUMS次;因此这里直接不写了只给个缺省参数,这样调试看起来就不会显示来回跳转初始化freelist数组的过程了,方便观察!//获得不用大小的内存块voidPush(void* ptr){//头插:assert(ptr);*(void**)ptr= _freelist; _freelist = ptr;}//弹出对应大小内存块void*Pop(){//头删:assert(_freelist);void* next =*(void**)_freelist;void* obj = _freelist; _freelist = next;return obj;}boolempty(){return _freelist==nullptr;}~FreeList(){}private:void* _freelist=nullptr;};

✅ ThreadCache设计

  • 大致思路:这里对于线程申请内存就往threadcache要,然后它就会向底层的freelist要,如果没有就会向Centralcache索取一块给对应的线程用,然后剩下的就挂到对应的freelist里面方便下次使用,回收内存就挂到对应freelist里面(可能多了填充到对应的CentralCache里面,这里先暂不做处理)

容量设计:

static constint MAX_BYTES =256*1024;//线程缓存最大256kb static constint FREELIST_NUMS =208;//哈希桶个数

整体大致设计:

class ThreadCache { public: void*Allocate(size_t size);//指针以及对应空间实际长度 void Deallocate(void* ptr, size_t size);//ThreadCache没了就向CentralCache索取 void*FetchFromCentralCache(size_t blocksize,size_t index); private: FreeList _freelists[FREELIST_NUMS];//哈希桶数组};

首先先看下对应的块的大小及分布的freelist的下标位置:

// 整体控制在最多10%左右的内碎片浪费// [1,128] 8byte对齐 freelist[0,16)// [128+1,1024] 16byte对齐 freelist[16,72)// [1024+1,8*1024] 128byte对齐 freelist[72,128)// [8*1024+1,64*1024] 1024byte对齐 freelist[128,184)// [64*1024+1,256*1024] 8*1024byte对齐 freelist[184,208)

这里在一定范围内就按照指定对齐数向上取整得到对应大小内存块,我们为了拥有更大的利用率,因此进行这样设计!

📊 其次就是这里还会出现两个问题:

  1. 申请内存块的时候大小不一定,如何找到对应freelist中真正内存块大小呢?
  • 上面我们说了,每次申请内存并不会直接给你对应大小的,比如你申请15;它会给你一个16的内存块,依此类推,可以理解成有个补齐的原则
  • 下面有个简单的算法,我们也不难想到:先找到对应的对齐数是多少,然后通过相除方法得到:

判断对齐数是多少:

//判断要开辟的真实字节数(也就是哪一个哈希桶) static size_t Real_Size(size_t size){if(size <=128){return_Real_Size(size,8);}elseif(size <=1024){return_Real_Size(size,16);}elseif(size <=8*1024){return_Real_Size(size,128);}elseif(size <=64*1024){return_Real_Size(size,1024);}elseif(size <=256*1024){return_Real_Size(size,8*1024);}else{assert(false);return-1;}}

获得对应的真实字节数:

 static size_t _Real_Size(size_t size,size_t alignnum){ size_t rsize;if(size %alignnum ==0) rsize = size;else rsize=(size / alignnum +1)* alignnum;return rsize;}

但是这种方法因为用到了除法,乘法等,对于要求非常高的时候,效率会是十分低的,因此改进成对应的位操作

 static inline size_t _Real_Size(size_t bytes, size_t alignNum){return((bytes + alignNum -1)& ~(alignNum -1));}

🔍下面进行解释下:

比如对齐数是8,拿到的是1-7,此时给它+7就是8-14;此时~(alignNum - 1)后最低位是8的位是1;此时8-14对应8的那位为1后面无论是几&后都是0;因此real_size就是8。

⏳一句话总结:

(bytes + alignNum - 1)完成对不足对齐数倍数进行进位,此时进了一位或者一位多一点& ~(alignNum - 1)干掉不足对齐数的那些位

  1. 如何根据想要申请的字节数找到对应真实内存块的freelist的下标呢?
  • 可以判断这个大小字节落在哪个对齐数的桶集内,然后通过求出它落在这个这个桶集的第几个freelist里面,然后再加上之前的那些桶的下标即可。

获得落在哪个桶集,并累加之前桶集的总数:

static size_t Index(size_t size){//相同对齐数的哈希桶分一组(前四种):int groups[4]={16,56,56,56};if(size <=128){return_Index(size,8);}elseif(size <=1024){return groups[0]+_Index(size-128,16);}elseif(size <=8*1024){return groups[0]+ groups[1]+_Index(size-1024,128);}elseif(size <=64*1024){return groups[0]+ groups[1]+groups[2]+_Index(size-64*1024,1024);}elseif(size <=256*1024){return groups[0]+ groups[1]+ groups[2]+groups[3]+_Index(size-256*1024,8*1024);}else{assert(false);return-1;}}

求出占了当前桶集的多少freelist:

//计算落在对齐数为alignnum的那些哈希桶中具体的哈希桶即freelist下标: static size_t _Index(size_t size, size_t alignnum){ size_t index;if(size % alignnum ==0) index = size / alignnum -1;else index = size / alignnum;return index;}

当然这里也是有优化的(按位操作):

static inline size_t _Index(size_t bytes, size_t alignnum){ size_t _2_top = static_cast<size_t>(std::log2(alignnum));//求alignnum是2的哪个指数次结果return((bytes +(1<< _2_top)-1)>>_2_top)-1;}

🔍解释下:

bytes是落在某个对齐数这个区间内的多余的字节(假设对齐数就是16),那么此时bytes(假设是16-31)+15一定落在 16 32 48 两个区间内, 其中要么bytes为16落在16 -32 剩下的就是在32-48 区间内,此时,进行除 对齐数,此时-1后,当bytes为16就是0,其余就是1;注意下标:因为第一种桶已经计算了是16个,这里-1就是多少个桶与下标之间的对应关系映射好!

⏳一句话总结:

(bytes + (1 << _2_top) - 1)为了给不足一个对齐数倍数的进位 ;>>_2_top 获得真正是哪个桶 - 1 完成是多少个桶与下标之间的对应关系映射。

🛠️Allocate实现
  • 通过对应的size获取实际内存块大小,并定位到哪个freelsit然后进行pop;如果freelist为空,那就去CentralCache对应大小块里面去申请,取出一个使用,剩下的挂回去(之后会结合CentralCache实现下,这里先不实现了)。
void* ThreadCache::Allocate(size_t size){assert(size <= MAX_BYTES);//小于256kb size_t index=Calculate::Index(size);//工具类里的函数必须设置静态才能访问否则由于this指针不能直接访问if(!_freelists[index].empty()){return _freelists[index].Pop();}else{//一定字节内存块的哈希桶空了,向中心缓存申请: size_t block_size = Calculate::Real_Size(size);returnFetchFromCentralCache(block_size, index);}}

FetchFromCentralCache: 从中心缓存获取内存块,这里暂不实现了,之后结合CentralCache原理实现!

🛠️Deallocate实现
  • 直接拿到这块不用的内存块,挂回对应的freelist(记住千万不要释放掉,后续还可能用到,它也没用这个权利)
void ThreadCache::Deallocate(void* ptr, size_t size){assert(ptr);assert(size); size_t index = Calculate::Index(size); _freelists[index].Push(ptr);}
🚀基于多线程私有ThreadCache设计
  • 首先,后期我们使用这个高并发内存池的时候,是多线程使用的,因此每个线程都要独立拥有一个ThreadCache,它们互相在这里面索取内存块,不用加锁,互不干扰。

因此我们进行这样的操作:

_declspec(thread) ThreadCache* TLSThreadCache = nullptr;

解释下:

其次就是_declspec(thread) win专属的,修饰的类型的对象是线程本地储存,也就是线程看到这个对象(被修饰的)就会把它搬到线程局部存储每个线程就不在共享这个全局变量而是私自占有不同的—>就产生了线程缓存(无锁)

但是这里还会出现个问题:重定义问题!

解决方法:

  • 这里需要搞成static;否则多个cpp包含这个头文件链接obj的时候每个cpp都有这个定义,互相能看到就重定义了,因此static一下只在本cpp有效互相不能看到了,避免重定义
static _declspec(thread) ThreadCache* TLSThreadCache = nullptr;

⚠️书写代码时值得注意的几个地方

  • 之前基于Linux,我们采取的是hpp(直接把h和cpp合并了,故类内直接写函数声明与定义即可)格式完成对类的分块处理,这里采取的是hcpp的形式(h里声明,cpp里定义):因此可以考虑在cpp写定义的时候需要声明这个函数的类名,剩下的就想象成在这个类内定义都是一样的!
  • 其次就是把多个模块经常用到的函数等封装成一个类(可以叫工具类),然后放在一个公共头文件里(common头文件);这样就大大简化了麻烦,直接包含这个文件,调用这个类函数就行;我们直接把它搞成静态成员函数,这样每次使用都不需要定义对象,直接声明类,使用这个函数即可[静态成员函数属于类本身,而不是类的某个特定对象。因此,你可以通过类名直接调用静态成员函数,而不需要创建类的实例。]

三·✨ThreadCache的多线程测试

  • 下面我们简单搞出两个线程来测试下,看是否它们都能拿到各自的ThreadCache,也就是看每个线程的TLSThreadCache是否是不同的即可(如果是不同的也就是每个线程都拿到了不同的ThreadCache这个类对象的地址,此时也就保证了它们各自有各自的线程缓存空间了,达成了互不干扰)。

搞出两个线程走从自己的ThreadCache申请内存:

void alloc1(){for(int i =0; i <3; i++){ void *ptr=ConcurrentAlloc(7);}} void alloc2(){for(int i =0; i <3; i++){ void* ptr =ConcurrentAlloc(10);}}intmain(){ std::thread t1(alloc1); t1.join(); std::thread t2(alloc2); t2.join();}

这两个线程都会走ConcurrentAlloc这个函数,但是由于TLSThreadCache线程局部存储,每个线程看到的都是自己的,当访问Allocate的时候,也是访问线程独有的(这里可以认为new的时候把这个函数已经保存在线程自己的局部储存里面了),因此此种情况下,Allocate是个可重入函数,多线程访问不会出现问题:

void*ConcurrentAlloc(size_t size){if(TLSThreadCache==nullptr) TLSThreadCache =new ThreadCache;//TLSThreadCache为线程局部自己的,因此每个线程都不一样,每次线程拿到的都是自己局部储存里面的TLSThreadCache,// 实现每个线程管理自己的ThreadCache,非共享的,无锁化 cout << std::this_thread::get_id()<<":"<< TLSThreadCache << endl;return TLSThreadCache->Allocate(size);}

👍测试效果

第一步先打断点在进行调试:

在这里插入图片描述

首先进行调试:

在这里插入图片描述

打开并行监视对TLSThreadCache指针进行并行监视:

在这里插入图片描述
在这里插入图片描述

然后观察对应的线程是否可以拿到只属于自己的threadcache对象指针,也就是验证是否每个线程有自己独立的threadcache

在这里插入图片描述

第一个线程走第二次申请内存块:

在这里插入图片描述


在这里插入图片描述
在这里插入图片描述

发现获取的和第一次创建的地址一样!

下面换到第二个线程 ,对应的线程tid是不同的,拿到的对应的threadcache对象地址也不同:

在这里插入图片描述

之后当第二个线程下一次再去threadcache申请内存拿到的仍是创建的那个对象地址,同上面,这里就不演示了!

看一下最后的效果:两个不同线程对应两个不同threadcache指针,因此验证了每个线程有自己独立的threadcache

在这里插入图片描述
  • 本次测试成功证明了每个线程都可以拿到自己独立的ThreadCache,之后我们就可以直接给他们分配内存块,实现多线程各自向自己的线程缓存去申请空间使用。

四· 📧本篇ThreadCache暂时的源码

🚀🚀点击跳转gitee速看源码🚀🚀

五· 🎨本篇小结

  • 通过本篇,学习到这个高并发内存池的整体框架,以及之间的相互联系,然后简单的实现了线程独有的ThreadCache这个模块的一小部分(后续还需要继续补充,这里便对它有了个认识),之后会更新关于CentralCachePageCache的设计实现等等,欢迎订阅!

究竟是什么样的终点才配的上这一路的颠沛流离!

Read more

Python 绘制动态跳动爱心|情人节专属浪漫代码,新手零基础也能上手

Python 绘制动态跳动爱心|情人节专属浪漫代码,新手零基础也能上手

马上就是情人节,程序员的浪漫从一行行代码开始!今天分享一款纯 Python 内置库实现的动态跳动爱心,无需复杂第三方依赖,黑色背景搭配粒子化爱心,自带自然的跳动节奏和柔和光晕,既适合送给心仪的人制造惊喜,也能作为 Python 基础练手案例。本文全程保姆级文本解析 + 代码注释双保障,从环境搭建到代码逻辑逐字拆解,纯新手也能跟着一步步实现,轻松拿捏编程浪漫~ 这是最近粉丝私信求表白代码的聊天记录 —— 情人节 / 过年想给心仪的人制造浪漫,用代码做一份专属爱心礼物再合适不过,安排! 一、效果预览 运行代码后会直接弹出640×480的独立图形窗口,黑色背景搭配粒子化粉色爱心,实现沉浸式浪漫视觉效果,核心效果如下: 1. 爱心以自然的周期性节奏跳动,完成“收缩-扩张-收缩”的循环,流畅无卡顿; 2. 爱心由大量细腻粒子构成,轮廓清晰、内部填充饱满,边缘带有轻微粒子扩散效果; 3. 爱心外围附带动态光晕,光晕的大小、粒子数量随爱心跳动节奏同步变化,氛围感拉满; 4. 全程动态渲染,对电脑性能无要求,低配设备也能流畅运行,关闭窗口即可停止程序。

By Ne0inhk

Anaconda环境变量PYTHONPATH设置:导入自定义PyTorch模块

Anaconda环境变量PYTHONPATH设置:导入自定义PyTorch模块 在深度学习项目开发中,一个看似微小的路径问题常常让开发者陷入“明明代码没错,却无法运行”的窘境。比如你在Jupyter Notebook里写好了模型结构、数据加载器和训练脚本,结果一执行就弹出ModuleNotFoundError: No module named 'models'——这种报错几乎每个PyTorch使用者都曾经历过。 问题的根源往往不在于代码逻辑,而在于Python解释器找不到你的自定义模块。尤其是在使用Anaconda虚拟环境或容器化镜像(如PyTorch-CUDA-v2.8)时,如果没有正确配置PYTHONPATH,即使文件结构清晰、类定义完整,也无法被正常导入。 这背后其实是一个典型的工程实践问题:如何让开发环境“理解”你组织代码的方式?答案就是合理利用PYTHONPATH这一机制,在不修改源码的前提下,打通模块搜索路径。 假设我们有一个标准的项目结构: /my_project ├── models/ │ └── custom_net.py ├── utils/ │ └──

By Ne0inhk

Semantic Kernel Python 进阶:Prompt 模板中的函数嵌套调用实战

发布日期: 2025年3月2日 关键词: Semantic Kernel, Python, Prompt Engineering, Function Calling, LLM 阅读时间: 约 15 分钟 前言 Microsoft 的 Semantic Kernel (SK) 提供了一个强大的特性:允许在 Prompt 模板中直接调用其他函数。这意味着你可以在一个 Semantic Function 的 Prompt 中嵌套调用其他 Semantic Functions 或 Native Functions,实现真正的函数式编程范式。 本文将深入讲解 SK Python 中的嵌套调用机制,并通过大量实战示例展示如何构建模块化的 AI 应用。 一、核心概念:Prompt 模板语法 Semantic Kernel

By Ne0inhk
一文掌握Python Flask:HTTP微服务开发从入门到部署

一文掌握Python Flask:HTTP微服务开发从入门到部署

Flask是一个轻量级的Python Web框架,以其"微内核"设计哲学闻名于世。所谓"微"并非指功能简单,而是指核心简洁、高度可扩展——Flask只提供最基础的Web服务能力,其他所有功能都可通过丰富的扩展生态系统按需添加。这种设计让开发者能够从几行代码的简单应用开始,逐步构建出复杂的企业级系统。 一、依赖库的安装与配置 1. 环境准备 首先确保已安装Python(建议版本3.7+),然后通过pip安装Flask依赖: # 安装 Flask pip install flask # 如果系统中有多个 Python 版本,可能需要使用 pip3 pip3 install flask 2. 验证安装 安装完成后,创建一个简单的应用来验证 Flask 是否正确安装并正常工作: # main.py# 导入Flask框架,Flask是一个轻量级的Python Web框架

By Ne0inhk