005 高并发内存池_CentralCache设计

​🌈个人主页:Fan_558
🔥 系列专栏:高并发内存池
🌹关注我💪🏻带你学更多知识

在这里插入图片描述

文章目录

  • 前言
    • 本文重点
    • 一、构建CentralCache结构
    • 二、运用慢开始反馈调节算法
    • 三、完成向CentralCache中心缓存申请
    • 四、承上启下
  • 小结

前言

本文将会带你走进高并发内存池的CentralCache的设计

本文重点

那我们在此模块将要完成以下任务:

1、结构上,我们需要设计CentralCache的结构——设计Span结构(双向带头链表)
2、对于CentralCache整个进程中只有一个,我们可以设计一个单例模式(饿汉)实现
3、设计慢开始反馈调节算法计算出centralcache应该给threadcache多少个对象
4、完成FetchFromCentralCache(向中心缓存申请内存对象)与FetchRangeObj(从CentralCache结构中获取一定数量的对象给threadcache)函数
5、承上启下

一、构建CentralCache结构

central cache与thread cache有两个明显不同的地方,首先,threadcache是每个线程独享的,而central cache是所有线程共享的,因为每个线程的threadcache没有内存了都会去找central cache,因此在访问central cache时是需要加锁的。

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

central cache与thread cache的第二个不同之处就是,thread cache的每个桶中挂的是一个个切好的内存块,而central cache的每个桶中挂的是一个个的span。而每个span中都会指向一个自由链表,自由链表链接的内存对象大小与桶一一对应

注意:centralcache的映射规则和threadcache是一样的,也就是说centralcache里面的哈希桶个数也是208,这样设计的好处是当线程向thread cache某个桶中申请内存对象时,如果没有内存了,就直接去central cache对应的哈希桶进行申请就可以了

在这里插入图片描述
每个线程都有一个属于自己的thread cache,我们是用TLS来实现每个线程无锁的访问属于自己的thread cache的。而central cache和page cache在整个进程中只有一个,对于这种只能创建一个对象的类,我们可以将其设置为单例模式。
  单例模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。单例模式又分为饿汉模式和懒汉模
式,懒汉模式相对较复杂,我们这里使用饿汉模式就足够了。

// 单例模式(饿汉
class CentralCache
{
public://提供一个全局访问点static CentralCache* GetInstance(){return &_inst;}//获取一个非空的SpanSpan* GetoneSpan(SpanList& list, size_t byte_size);// 从中心缓存获取一定数量的对象给thread cachesize_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);private:SpanList _spanLists[FreeListBucket];
private:CentralCache(){}//禁掉拷贝CentralCache(const CentralCache&) = delete;//声名static CentralCache _inst;
};

CentralCache.cpp中存在一个CentralCache类型的静态的成员变量,当程序运行起来后此对象被立马创建,此后程序中就只有这一个单例了。

CentralCache CentralCache::_inst;

看到这里你或许会有疑问?
span是什么呢,span在英文里是跨度的意思,span是一个管理以页为单位的大块内存,通常用于表示一段连续的内存块
span的结构如下:

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

对于span管理的以页为单位的大块内存,我们需要知道这块内存具体在哪一个位置,便于之后page cache进行前后页的合并,因此span结构当中会记录所管理大块内存起始页的页号 _pageId

至于每一个span管理的到底是多少个页,这并不是固定的,需要根据多方面的因素来控制,因此span结构当中有一个 _n成员,该成员就代表着该span管理的页的数量。

此外,每个span管理的大块内存,都会被切成相应大小的内存块挂到当前span的自由链表中,比如8Byte哈希桶中的span,会被切成一个个8Byte大小的内存块挂到当前span的自由链表中,因此span结构中需要存储切好的小块内存的自由链表 _freeList

span结构当中的 _useCount成员记录的就是,当前span中切好的小块内存,被分配给thread cache的计数,当某个span的_useCount计数变为0时,代表当前span切出去的内存块对象全部还回来了,此时central cache就可以将这个span再还给page cache。

每个桶当中的span是以双链表的形式组织起来的,当我们需要将某个span归还给page cache时,就可以很方便的将该span从双链表结构中移出。如果用单链表结构的话就比较麻烦了,因为单链表在删除时,需要知道当前结点的前一个结点
_next_prev

在CentralCache结构中,其中每一个哈希桶里面存储的都是一个个span,而这些span用双链表链接起来,我们可以对此进行封装
SpanList结构
在此我们只简单地创建了一个双链表,并提供了两个基础的函数

//带头双向循环链表
class SpanList
{
public://初始化双向链表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;}//头删void Erase(Span* pos){assert(pos);Span* prev = pos->_prev;Span* next = pos->_next;prev->_next = next;next->_prev = prev;//不需要真正delete该pos处的span,可能需要还给pagecache}
private:Span* _head;
public:std::mutex _mtx; //桶锁
};

关于页号的类型:PAGE_ID _pageId

每个程序运行起来后都有自己的地址空间,在32位平台下,进程地址空间的大小为2^32,而64位平台下,进程地址空间的大小为 2 ^64 页的大小一般是4K或者是8K,以8K为例,32位平台:进程地址空间就可以分成2^32 ÷2^13 = 2^ 19个页,在64位平台:进程地址空间被分成2^ 64÷2^13 = 2^51个页,页号本质和地址是一样的,都只是一个编号,只是地址以一个字节为一个单位,而页是以多个字节为一个单位
由于页号在64位平台下的取值范围是[0,2^51],我们需要用条件编译来解决这个问题

#ifdef _WIN64
typedef unsigned long long PAGE_ID;
#elif _WIN32
typedef size_t PAGE_ID;
#else
//linux
#endif

值得注意的是,在32位下,_WIN32有定义,_WIN64没有定义;而在64位下,_WIN32和_WIN64都有定义。因此在条件

二、运用慢开始反馈调节算法

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

这里可以联想threadcache与centralcache结构来思考,虽然CentralCache拿span中自由链表里一个内存对象给ThreadCache就够用了,但是不保证下次还会来要
在这里插入图片描述
因此,我们这里采用了一个慢开始反馈调节算法。当thread cache向central cache申请内存时,如果申请的是较小的对象,那么可以多给一点,但如果申请的是较大的对象,就可以少给一点。
 通过下面这个函数,我们就可以根据所需申请的对象的大小计算出具体给出的对象个数,并且可以将给出的对象个数控制到2~512个之间。也就是说,就算thread cache要申请的对象再小,我最多一次性给出512个对象;就算thread cache要申请的对象再大,我至少一次性给出2个对象。

class SizeClass
{
public://thread cache一次从central cache获取对象的上限static size_t NumMoveSize(size_t size){assert(size > 0);//对象越小,计算出的上限越高//对象越大,计算出的上限越低size_t num = MAX_BYTES / size;if (num < 2)num = 2;if (num > 512)num = 512;return num;}
};

但就算申请的是小对象,一次性给出512个也是比较多的,基于这个原因,我们可以在FreeList结构中增加一个叫做_maxSize的成员变量,该变量的初始值设置为1,并且提供一个公有成员函数用于获取这个变量。也就是说,现在thread cache中的每个自由链表都会有一个自己的_maxSize。

class FreeList
{
public:size_t& MaxSize(){return _maxSize;}private:void* _freeList = nullptr;	//指向自由链表的指针size_t _maxSize = 1;	};
FetchFromCentralCache.cpp:
size_t batchNum = std::min(_freeLists[index].MaxSize(), SizeClass::NumMoveSize(size));
if (batchNum == _freeLists[index].MaxSize())
{++_freeLists[index].MaxSize();
}

通过比较Max_Size和NumMoveSize返回的上限,取出二者之间的最小值,thread cache第一次向central cache申请某大小的对象时,申请到的都是一个,但下一次thread cache再向central cache申请同样大小的对象时,因为该自由链表中的_maxSize增加了,最终就会申请到两个。直到该自由链表中_maxSize的值,增长到超过NumMoveSize函数计算出的值后就不会继续增长了,此后申请到的对象个数就是NumMoveSize函数计算出的个数。

三、完成向CentralCache中心缓存申请

每次threadcache向centralcache申请对象时,先通过慢开始反馈调节算法计算出本次应该申请的对象的个数,然后再通过FetchRangeObj查看真实情况下centralcache对应桶中span的自由链表上有几个内存对象(actualNum),如果只有一个就直接返回。

//从中心存储中获取
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{//慢开始反馈调节算法//选出合适的对象申请数,慢开始size_t batchNum = std::min(_freeLists[index].MaxSize(), SizeClass::NumMoveSize(size));if (batchNum == _freeLists[index].MaxSize()){++_freeLists[index].MaxSize();}void* start = nullptr;void* end = nullptr;//实际能从CentralCache中获取到的对象数size_t actualNum = CentralCache::GetInstance()->FetchRangeObj(start, end, batchNum, size);//将从CentralCache中获取到的对象数给ThreadCacheassert(actualNum >= 1);		if (actualNum == 1){assert(start == end);return start;}//将第一个对象返回以外,还需要将剩下的对象挂到threadcache对应的哈希桶中else{_freeLists[index].PushRange(FreeList::NextObj(start), end, size);return start;}
}

如果申请到多个对象,除了将第一个对象返回以外,还需要将剩下的对象挂到threadcache对应的哈希桶中。根据需求,我们需要向封装的自由链表继续添加一个函数PushRange将多内存对象链接到对应的桶中

	//将自由链表链接到ThreadCache的桶中void PushRange(void* start, void* end, size_t n){NextObj(end) = _freeList;_freeList = start;}

FetchRangeObj函数将central cache对应的哈希桶中span里freeList链接的内存对象数量返回

//从中心缓存中申请
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{//根据对齐规则选择从哪一个桶拿(threadcache与centralcache的对齐规则相同)size_t index = AlignmentRules::Index(size);//加桶锁(由于centralcache只有一个,threadcache向centralcache申请内存时可能面临着多个线程向centralcache同一个桶申请)_spanLists[index]._mtx.lock();Span* span = CentralCache::GetoneSpan(_spanLists[index], size);assert(span);	//保证span不为空assert(span->_freeList);	//保证span对象中自由链表_freeList不为空start = span->_freeList;end = start;//返回actual个对象,有多少返回多少size_t actualNum = 1;size_t i = 0;while (i < batchNum - 1 && FreeList::NextObj(end) != nullptr){end = FreeList::NextObj(end);i++;actualNum++;}//将分配剩余的对象重新挂在span中span->_freeList = *(void**)end;*(void**)end = nullptr;//为释放流程作准备span->_useCount += actualNum;_spanLists[index]._mtx.unlock();return actualNum;
}

值得注意的是,我们实际申请到的内存对象数可能是比通过慢开始反馈调节算法计算出的batchNum要少的,但这不会产生什么影响,有多少拿多少,thread
cache的本意就是向central cache申请一个对象,之所以一次要申请多个内存对象,是因为这样的话下一次直接可以在threadcache中获取了

四、承上启下

在FetchRangeObj函数中调用了GetoneSpan函数,GetoneSpan函数用来获取一个非空的span,一开始会先遍历span的链表结构,如果span不为空就返回一个span给FetchRangeObj函数,如果为空就要向下一层进行申请PageCache

Span* CentralCache::GetoneSpan(SpanList& list, size_t byte_size)
{//从list中取出一个非空的span,遍历Span* it = list.Begin();while (it != list.End()){//存在非空的span就返回if (it->_freeList != nullptr){return it;}else it = it->_next;}...本函数还有一些步骤,以下就是向PageCache页缓存中申请,且听下回分解
}

小结

今日的项目分享就到这里啦,迄今为止也已经介绍了ThreadCache与CentralCache,大家一定一定要把结构给理解好,欲知PageCache,且听下回分解

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/788761.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【JavaScript】函数 ③ ( 形参 与 实参 匹配问题 | 实参个数 = 形参个数 | 实参个数 > 形参个数 | 实参个数 < 形参个数 )

文章目录 一、JavaScript 函数 形参 与 实参 匹配问题1、函数形参与实参不匹配问题2、形参与实参个数匹配3、实参个数 > 形参个数4、实参个数 < 形参个数5、完整代码示例 一、JavaScript 函数 形参 与 实参 匹配问题 1、函数形参与实参不匹配问题 在 其它语言 中 , 如 Ja…

C语言 | Leetcode C语言题解之第6题Z字形变换

题目&#xff1a; 题解&#xff1a; char * convert(char * s, int numRows){int n strlen(s), r numRows;if (r 1 || r > n) {return s;}int t r * 2 - 2;char * ans (char *)malloc(sizeof(char) * (n 1));int pos 0;for (int i 0; i < r; i) { // 枚举矩阵的…

上位机图像处理和嵌入式模块部署(qmacvisual之n点标定)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 工业场景中&#xff0c;很多时候图像是用来做测量的。虽然我们很希望载台是平的&#xff0c;摄像头是正对着拍摄物体的&#xff0c;但是运行时间长…

AssetBundle在移动设备上丢失

1&#xff09;AssetBundle在移动设备上丢失 2&#xff09;Unity云渲染插件RenderStreaming&#xff0c;如何实现多用户分别有独立的操作 3&#xff09;如何在圆柱体类型的地图中编程玩家的输入 4&#xff09;Mixamo动画的根运动问题 这是第380篇UWA技术知识分享的推送&#xff…

离散化、贪心、双指针、二分、倍增、构造、位运算

八、离散化 1、离散化简介 把无限空间中有限的个体映射到有限的空间中去,以此提高算法的时空效率。离散化是一种将数组的值域压缩,从而更加关注元素的大小关系的算法。当原数组中的数字很大、负数、小数时(大多数情况下是数字很大),难以将“元素值”表示为”数组下标“,一…

为什么mac文件拖拽不了 mac文件拖不进硬盘里 macbookpro文件无法拖进移动硬盘 Tuxera NTFS for Mac 2023绿色

如果你是一位Mac用户&#xff0c;你可能会遇到这样的问题&#xff1a;你想把Mac上的文件拖拽到其他位置&#xff0c;比如桌面、文件夹或者外接硬盘&#xff0c;但是却发现无法操作&#xff0c;这是为什么呢&#xff1f;这篇文章将为你解答为什么mac文件拖拽不了&#xff0c;以及…

WebGIS 地铁交通线网数据可视化监控平台

数字孪生技术在地铁线网的管理和运维中的应用是一个前沿且迅速发展的领域。随着物联网、大数据、云计算以及人工智能技术的发展&#xff0c;地铁线网数字孪生在智能交通和智慧城市建设中的作用日益凸显。 图扑软件基于 HTML5 的 2D、3D 图形渲染引擎&#xff0c;结合 GIS 地图&…

EFK(elasticsearch+filebeat+kibana)日志分析平台搭建

本文是记录一下EFK日志平台的搭建过程 项目背景&#xff1a; 此次搭建的日志分析平台主要是采集服务器上的java服务的log日志(输出的日志已经是json格式)&#xff0c;这些日志都已经按照不同环境输出到/home/dev /home/test1 /home/test2 目录下了&#xff0c;按照不同的应…

redis链表结构和简单动态字符串(SDS)

1.双向链表 redis中的普通链表是双向链表。通过链表节点结构体可知有全驱节点和后继节点。 1.链表节点和链表 //adlist.h typedef struct listNode {struct listNode *prev; //前驱节点struct listNode *next; //后继节点void *value; //节点值 } list…

27.ReentrantLock

1.与synchronized不同点&#xff1a; 可中断可以设置超时时间可以设置公平锁&#xff0c;公平锁就是为了解决饥饿线程&#xff0c;让线程排队&#xff0c;先进先出&#xff0c;先来的线程先执行。支持多个条件变量 2.与synchronized相同点都支持锁的可重入。 基本格式&#…

“崖山数据库杯”深圳大学程序设计竞赛(正式赛)M题 一图秒

“崖山数据库杯”深圳大学程序设计竞赛&#xff08;正式赛&#xff09;_ACM/NOI/CSP/CCPC/ICPC算法编程高难度练习赛_牛客竞赛OJ (nowcoder.com) —————— 可以去牛客看题解&#xff1a; 题解 | #暂时没想法#_牛客博客 (nowcoder.net) —————— 上面的就是题解了。…

Web CSS笔记3

一、边框弧度 使用它你就可以制作盒子边框圆角 border-radius&#xff1a;1个值四个圆角值相同2个值 第一个值为左上角与右下角&#xff0c;第二个值为右上角与左下角3个值第一个值为左上角, 第二个值为右上角和左下角&#xff0c;第三个值为右下角4个值 左上角&#xff0c;右…

springboot之MybatisPlus

文章目录 一、ORM二、mybatis实际操作三、mybatis-plus 一、ORM 简单来说ORM就是一个能够帮我们把java中Bean类映射到数据库中。 使用mybatis-plus。 配置架包 <!-- MyBatisPlus依赖 --><dependency><groupId>com.baomidou</groupId><art…

垄断与商品化背景下的网络安全三大整合策略

我国的网络安全产业已经发展了20余年&#xff0c;大大小小的企业几乎覆盖了网络安全的所有领域。随着安全需求的逐渐递增&#xff0c;安全产品也朝着平台化、规模化发展&#xff0c;这就倒逼着安全厂商需要整合越来越多的安全能力&#xff0c;并与其产品相融合。这个过程&#…

【VSCode+Keil5+STM32CubeMX】开发环境配置

一、软件下载 二、软件安装 三、配置环境 四、验证开发环境 五、Keil与VS Code的同步 从0到1搭建VS Code Keil5 STM32CubeMX开发环境 优点 支持标准库HAL库LL库代码编辑更“现代化”&#xff1a;代码提示、函数跳转、更高自由度的定制主题等优点多端同步&#xff0c;VS Code和…

【LAMMPS学习】七、加速性能(3)通用技巧

7. 加速性能 7.1.基准测试 7.2.测试性能 7.3.通用技巧 以下是提高模拟性能的通用技巧。它们中的大多数只适用于当前性能中的某些模型和某些瓶颈&#xff0c;因此让您生成的计时数据作为指导。要预测这些选项会产生多大的差异&#xff0c;即使不是不可能&#xff0c;也是很难…

Jmeter02-1:参数化组件CVS

目录 1、Jmeter组件&#xff1a;参数化概述 1.1 是什么&#xff1f; 1.2 为什么&#xff1f; 1.3 怎么用&#xff1f; 2、Jmeter组件&#xff1a;参数化实现之CSV Data Set Config(重点中重点) 2.1 是什么&#xff1f; 2.2 为什么&#xff1f; 2.3 怎么用&#xff1f; …

Golang | Leetcode Golang题解之第5题最长回文子串

题目&#xff1a; 题解&#xff1a; func longestPalindrome(s string) string {if s "" {return ""}start, end : 0, 0for i : 0; i < len(s); i {left1, right1 : expandAroundCenter(s, i, i)left2, right2 : expandAroundCenter(s, i, i 1)if ri…

Mysql数据库getshell方法

今天摸鱼时候&#xff0c;突然有人问我不同的数据库getshell的方式&#xff0c;一时间我想到了mysql还有redis未授权访问到getshell的方式&#xff0c;但是仅仅第一时间只想到了这两种&#xff0c;我有查了查资料&#xff0c;找到了上面两种数据库getshell的补充&#xff0c;以…

【面试八股总结】传输控制协议TCP(三)

参考资料 &#xff1a;小林Coding、阿秀、代码随想录 一、TCP拥塞控制⭐ 1. 慢启动 – Slow Start 慢启动是指TCP连接刚建立&#xff0c;一点一点地提速&#xff0c;试探一下网络的承受能力&#xff0c;以免直接扰乱了网络通道的秩序。 慢启动算法&#xff1a; 初始拥塞窗口…