【项目日记】高并发内存池---实现内存回收

2024-09-05 02:04

本文主要是介绍【项目日记】高并发内存池---实现内存回收,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在这里插入图片描述

如果你不能飞,那就跑;
如果跑不动,那就走;
实在走不了,那就爬。
无论做什么,你都要勇往直前。
--- 马丁·路德·金 ---

高并发内存池---内存回收机制

  • 1 前情提要
  • 2 线程缓存的内存回收
  • 3 中心缓存的内存回收
  • 4 页缓存的内存回收
  • 5 调试检查

1 前情提要

前面我们实现了高并发内存池的三层结构:线程缓存,中心缓存,页缓存:

  1. 线程缓存:每个线程中都有的一个内存块链表数组,按照TLS(线程本地存储)设计。根据申请的size对齐后的大小找到对应的链表。如果有没有使用的内存块直接使用,没有就去中心缓存中进行申请一批内存块!
  2. 中心缓存: 所有线程共同使用一个中心缓存,其本质是spanlist(span用来管理大块内存和内存块)数组,按照单例模式设计。根据线程缓存申请内存块的size找到对应的spanlist,如果有合适的span就返回span中的一批内存块,没有就向页缓存中进行申请对应页数的span!
  3. 页缓存:所有线程共同使用一个页缓存,按照单例模式设计,其本质是一个管理不同页数span的链表数组。根据中心缓存申请的页数找到对应的spanlist,如果有就直接返回,没有就去页数更大的链表中查找是否有,有就进行拆分,如果没有向系统申请一块最大的span。

在这里插入图片描述
在这里,特别强调一下span。申请和回收操作中都会依赖span这个特殊结构:

struct Span
{//起始页的页号PAGE_ID _pageid = 0;//页的数量size_t _n = 0;Span* _next = nullptr;Span* _prev = nullptr;//引用计数size_t _usecount = 0;//被切分好的对象的大小size_t _objsize = 0;//储存切分好的对象的自由链表void* _freelist = nullptr;//是否被使用bool _isuse = false;
};
  • _pageid页号:在向系统申请空间时,是按照页来申请的。当我们有了一个页号在乘以页的大小就可以找到页的起始位置,在这里页的大小是2^13,即向左移动13位就可以得到页空间的起始位置
  • _n页数:表明span管理着多少页,即管理着多大的空间!

根据这两个成员变量我们就可以确定span管理的空间范围,然后就可以在中心缓存中将他们按照对应内存块的大小插入到 _freelist自由链表中!

好的,接下来我们就来进行回收机制的处理

2 线程缓存的内存回收

我们明确几个要素:

  1. 线程缓存回收的是内存块,将内存块重新挂载到对应的自由链表中。
  2. 线程缓存中的自由链表挂载着内存块,当对应链表挂载的内存块数量超出一个标准,就需要进行回收。
  3. 线程缓存回收不是进行释放,而是将这一串内存块从自由链表中取出,传回给中心缓存,让中心缓存将这一批内存块重新储存起来!

根据这三个要素,我们可以写出一个线程回收的代码。我们就按照:当挂载的数量超出了自由链表申请内存块的最大数量,就释放所有挂载的内存块。释放时需要获取到这一串内存块链表的头尾节点地址,方便后续中心缓存处理!

void ThreadCache::Deallocate(void* ptr, size_t size)
{assert(ptr);assert(size < MAX_BYTES);//根据Index找到对应桶size_t index = SizeClass::Index(size);_freelist[index].Push(ptr);//如果_freelist[index]中的挂载的内存块过多 进行回收if (_freelist[index].Size() >= _freelist[index].MaxSize()){ListTooLong(_freelist[index], size);}}
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{//将_freelist中所有的内存块进行回收void* start = nullptr; // 开始节点的地址void* end = nullptr;   // 结束节点的地址list.PopRange(start, end, list.Size());//已经从_freelist[index]中将 size() 个数的内存块取出来//起始地址为start 结束地址是 end//将start 到 end 的内存块返回给中心缓存CentralCache::GetInstance()->ReleaseListToSpan(start, size);return;
}

原理图大致是这样!线程缓存中已经将需要释放的内存块链表传给了中心缓存,线程缓存已经完成了他的使命!
在这里插入图片描述

3 中心缓存的内存回收

对于中心缓存我们也要明确几个要素:

  1. 中心缓存需要处理线程缓存传来的一串内存块,通过内存块的地址找到其对应的页,通过页 - span的映射哈希表找到对应的span,进行回收
  2. 中心缓存中的spanlist是挂载着span的,当一个span的引用计数回到0时,就对其进行释放。
  3. 中心缓存释放span不是真的进行释放,是将其从中心缓存中取出来,还给页缓存进行处理

根据这三点要素,我们可以构建其基础的框架。处理的内部还有很多细节:;

  • 中心缓存是临界区,需要使用桶锁将桶锁住,离开中心缓存,进行页缓存处理时还要解桶锁,加上页缓存的锁!
  • 需要遍历内存块链表,依次进行处理。因为这些内存块不一定属于同一个span。每还回一个内存块需要减少引用计数!
  • 当引用计数为0时,调用页缓存对该span进行处理!
void CentralCache::ReleaseListToSpan(void* start, size_t size)
{//将start指向的内存块链表 头插到对应的链表中size_t index = SizeClass::Index(size);//上锁_spanlists[index].GetMutex().lock();//遍历链表while (start != nullptr){void* next = NextObj(start);Span* span = PageCache::GetInstance()->MapObjectToSpan(start);//找到对应的span之后//将数据块头插到span中的_freelist中!NextObj(start) = span->_freelist;span->_freelist = start;span->_usecount--;//start向后移动!start = next;//判断span是否需要回收if (span->_usecount == 0){_spanlists[index].Erease(span);//进行回收span->_freelist = nullptr;span->_next = nullptr;span->_prev = nullptr;//页缓存进行回收_spanlists[index].GetMutex().unlock();//页锁PageCache::GetInstance()->GetMutex().lock();PageCache::GetInstance()->ReleaseSpanToPageCache(span);PageCache::GetInstance()->GetMutex().unlock();//重新上锁_spanlists[index].GetMutex().lock();}}_spanlists[index].GetMutex().unlock();
}

原理图为:
在这里插入图片描述

4 页缓存的内存回收

页缓存中我们也明确几个要素:

  1. 页缓存是管理不同页数的span,根据我们申请span的过程,我们知道span都是来源于原始的一个128页span的,所以每当还回一个span,就要进行span的合并
  2. 合并过程根据span的页号和页数找到前面和后面的span,如果他们没有在使用就可以进行合并。合并后将被合并的span进行释放
  3. 合并的过程是要合并到不能合并为止!

页缓存的合并需要完成前后两部分的合并,为了保证现在可以快递找到的页号对应的span,需要在页缓存创建span的模块中加入将未使用的span建立首尾页号的映射

//将nspan的 首尾页号 进行映射
//方便PageCache回收span时进行合并
_SpanMap[nspan->_pageid] = nspan; //方便向后查找
_SpanMap[nspan->_pageid + nspan->_n - 1] = nspan;//方便先前查找

这样就可以完成页缓存的内存回收了!

void PageCache::ReleaseSpanToPageCache(Span* span)
{//将回收的span进行合并// 向前合并 + 向后合并//进行合并的基础是前后的span都通过哈希表进行了映射//所以要在PageCache传给CentralCache时将span的首尾页号进行映射//向前寻找while (1){PAGE_ID prev = span->_pageid - 1;auto ret = _SpanMap.find(prev);//检查是否查找到if (ret == _SpanMap.end()){break;}Span* pspan = ret->second;//检查是否在使用if (pspan->_isuse == true){break;}//防止合并的页过大 , 超出管理的范围if (pspan->_n + span->_n > PAGENUM - 1){break;}//进行合并span->_pageid = pspan->_pageid;span->_n += pspan->_n;//将被合并页释放_pageList[pspan->_n].Erease(pspan);//取消哈希映射_SpanMap.erase(pspan->_pageid);delete pspan;//继续向前合并}//向后合并while (1){PAGE_ID next = span->_pageid + span->_n;auto ret = _SpanMap.find(next);//检查是否查找到if (ret == _SpanMap.end()){break;}Span* nspan = ret->second;//检查是否在使用if (nspan->_isuse == true){break;}//防止合并的页过大 , 超出管理的范围if (nspan->_n + span->_n > PAGENUM - 1){break;}//进行合并span->_pageid = nspan->_pageid;span->_n += nspan->_n;//将被合并页释放_pageList[nspan->_n].Erease(nspan);//取消哈希映射_SpanMap.erase(nspan->_pageid);delete nspan;//继续向后合并}//合并完成 --- 将span挂载到合适位置_pageList[span->_n].PushFront(span);_SpanMap[span->_pageid] = span;_SpanMap[span->_pageid + span->_n - 1] = span;span->_isuse = false;//回收完成!!!
}

原理图为:
在这里插入图片描述
这样就完成了最终的页缓存的内存回收!!!

5 调试检查

调试检查的过程是很繁琐的,需要一步一步对代码的结果进行处理,首先编写一个简单的demo,然后进入调试,一步一步的进行调试。我调试老很久,解决2个主要的bug:

  1. ListTooLong函数参数没有传引用,导致对中心缓存的span的引用计数无法改变,导致程序崩溃!一开始以为是中心缓存没有对_usecount进行处理的问题。
  2. 线程缓存调用_freelist[index].Pop()时没有写[index],导致一开始测试1-8字节的时候没有出错,测试较大字节的时候程序崩溃!

经过漫长的Debug过程,最终是终于是在调试中确认了内存回收过程没有问题!

接下来就来测试多线程情况下能否成功运行:
在这里插入图片描述
没有问题!!!

这样高并发内存池的核心框架我们就写好了!!!
后续会继续进行性能测试和性能优化!!!敬请期待!!!

这篇关于【项目日记】高并发内存池---实现内存回收的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1137653

相关文章

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

NameNode内存生产配置

Hadoop2.x 系列,配置 NameNode 内存 NameNode 内存默认 2000m ,如果服务器内存 4G , NameNode 内存可以配置 3g 。在 hadoop-env.sh 文件中配置如下。 HADOOP_NAMENODE_OPTS=-Xmx3072m Hadoop3.x 系列,配置 Nam

如何用Docker运行Django项目

本章教程,介绍如何用Docker创建一个Django,并运行能够访问。 一、拉取镜像 这里我们使用python3.11版本的docker镜像 docker pull python:3.11 二、运行容器 这里我们将容器内部的8080端口,映射到宿主机的80端口上。 docker run -itd --name python311 -p

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

离心萃取机废旧磷酸铁锂电池回收工艺流程

在废旧磷酸铁锂电池的回收工艺流程中,离心萃取机主要应用于萃取除杂的步骤,以提高回收过程中有价金属(如锂)的纯度。以下是结合离心萃取机应用的废旧磷酸铁锂电池回收工艺流程: 电池拆解与预处理 拆解:将废旧磷酸铁锂电池进行拆解,分离出电池壳、正负极片、隔膜等部分。破碎与筛分:将正负极片进行破碎处理,并通过筛分将不同粒径的物料分开,以便后续处理。 浸出与溶解 浸出:采用适当的浸出工艺(如二段式逆

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount