首页 > 系统相关 >【项目日记】高并发内存池---实现中心缓存

【项目日记】高并发内存池---实现中心缓存

时间:2024-09-01 09:56:57浏览次数:11  
标签:链表 缓存 span --- 线程 内存 size

在这里插入图片描述

年少的梦啊,有些很幸运地实现了,有些被遗忘在了风中 --- 董卿 ---

高并发内存池---实现中心缓存

1 整体理念

实现中心缓存之前,我们先理解中心缓存需要做那些事情,具有哪些特性?我们把中心缓存的功能特性理解清楚了自然而然的就可以写出代码来!

首先,中心缓存和线程缓存不一样,不是每个线程都拥有自己的独一缓存,而是每个线程都可以进行访问,可以向中心缓存发出请求!所以中心缓存最好也必须只有一个,所有的线程都去这唯一一个中心缓存中进请求和释放内存块,效率更高,逻辑更加清晰!!!

  • 中心缓存应该按照单例模式进行设计,可以使用懒汉模型也可以使用饿汉模型!

中心缓存中,也有一个类似哈希表的结构,其中储存的不是内存块,而是一种特别的对象span(以页为单位的大块内存),这里面保护一个自由链表。也就是说span是一个大内存块,里面有很多小内存块供线程缓存取用。为了适配线程缓存的自由链表数,中心缓存采取同样的对齐映射规则,依旧是208个链表!

  • 中心缓存中有一个spanlist数组 ,每个链表中储存若干个spanspan中有一个自由链表,可以取出对应大小的内存块供线程缓存进行使用。这里的spanlist采取带头双向循环链表,让操作更加灵活!

线程缓存向中心缓存请求内存块是一个高并发场景,会存在线程安全的问题!所以需要加入锁,但是一般的互斥锁会直接锁住整个中心缓存,会造成性能的极大下降,因为每次请求的内存块只会在一个``spanlist`中,不会影响其他的链表,所以这里使用桶锁更加合适,只锁住一个桶,而不影响中心缓存中其他链表的请求!

  • spanlist中需要加入桶锁,请求内存时要锁住,保证线程安全!!!

在实际场景中线程缓存会不断的向中心缓存请求内存块,如果一次只给一块,那么就会导致频繁的请求加锁,导致性能变慢!为高并发内存池性能的关键是线程缓存中储存的内存块,我们一次多給一些,就能保证效率!同时为了保证内存最大限度的合理使用,我们采取慢调度开始算法,让线程申请的内存块逐渐增加,而不是一下子就给很多!!!

  • 需要加入慢调度开始算法,保证效率!

2 SpanList的实现

我们先来实现最底层的span

  1. 要支持双向链表,内部加入前后指针
  2. 加入页号和页的数量,便于分辨与统计
  3. 加入引用计数,用来判断span是否可以进行回收
  4. 内部有一个自由链表,储存切分好的内存块!
struct Span
{
	//起始页的页号
	PAGE_ID _pageid = 0;
	//页的数量
	size_t n = 0;
	//双向链表结构
	Span* _next = nullptr;
	Span* _prev = nullptr;

	//引用计数
	size_t _usecount = 0; //被请求 ++ 被归还-- 为0时可以进行整合
	//被切分好的对象的大小
	size_t _objsize = 0;
	//储存切分好的对象的自由链表
	void* _freelist = nullptr;

	//是否被使用
	bool _isuse = false;
	
};

基于这个span,我们来实现一个带头双向链表:

  1. 为了方便头插和尾插,我们直接写一个万能的插入函数
  2. 再写一个删除函数
  3. 加入桶锁,一定一定要加入桶锁!并写一个获取锁的接口!
class SpanList
{
public:
	//构造函数
	SpanList()
	{
		_head = new Span;
		_head->_prev = _head;
		_head->_next = _head;
	}

	// --- 迭代器 ---
	
	//插入节点
	void Insert(Span* pos , Span* newspan)
	{
		assert(pos);
		assert(newspan);
		//插入到pos前面
		Span* prev = pos->_prev;
		//进行插入
		prev->_next = newspan;
		newspan->_prev = prev;

		newspan->_next = pos;
		pos->_prev = newspan;
	}
	//删除节点
	void Erease(Span *pos)
	{
		assert(pos);
		//将pos节点删除
		Span* prev = pos->_prev;
		Span* next = pos->_next;

		prev->_next = next;
		next->_prev = prev;
	}
	//获取锁!
	std::mutex& GetMutex()
	{
		return _mtx;
	}
private:
	//头节点
	Span* _head;
	//加入桶锁
	std::mutex _mtx;
};

3 CentralCache的实现

我们先搭建起一个框架:

//所有线程共用一个中心缓存
class CentralCache
{
public:
	//用来为线程缓存提供内存块
	size_t FetchRangeobj(void* start, void* end, size_t batchnum, size_t size);
	//向页缓存获取 span 对象
	Span* GetOneSpan(SpanList& list, size_t size);
private:
	//span链表数组 与 线程缓存的自由链表数组一一对应
	SpanList _spanlists[LISTNUM];

};

其中函数GetOneSpan先不需要管,等我们实现页缓存再来进行联动!!!
FetchRangeobj函数一会来实现,我们先把上层线程缓存的联动调用写好!!!

现在我们先把CentralCache设计为单例模式,这里采取懒汉模式进行处理!

  • 注意:类内静态变量一定要在类外进行定义声明
  1. 首先我们在类内加入一个静态变量,整个类都只能有这一个对象!
  2. 然后为了保证“整个类都只能有这一个对象”,需要把构造函数进行私有化,并且删除拷贝构造和赋值拷贝
  3. 最后提供一个接口,保证每次调用时可以获取到这唯一的一个类对象!在第一次调用时进行创建,这里需要加锁保护,因为调用可以在不同线程内同时进行,要保证线程安全!
//所有线程共用一个中心缓存
//所以按照单例模式 --- 懒汉模型来进行设计
class CentralCache
{
public:
	static CentralCache* GetInstance()
	{
		双检查提高性能
		if (_pinfo == nullptr)
		{
			//上锁
			std::unique_lock<std::mutex> lock(_single_mtx);
			if (_pinfo == nullptr)
			{
				_pinfo = new CentralCache;
			}
		}
		//_pinfo = new CentralCache;
		return _pinfo;
	}
	size_t FetchRangeobj(void* start, void* end, size_t batchnum, size_t size);
	Span* GetOneSpan(SpanList& list, size_t size);
private:
	//span链表数组 与 线程缓存的自由链表数组一一对应
	SpanList _spanlists[LISTNUM];

private	:
	//单例模式设计
	
	//构造函数私有化
	CentralCache() {}
	//拷贝构造 赋值重载 Delete
	CentralCache operator=(const CentralCache& ) = delete;
	CentralCache(const CentralCache&) = delete;

	//唯一静态对象
	static CentralCache* _pinfo;
	//锁对象来保证单例模式创建时的线程安全
	static std::mutex _single_mtx;
};

4 请求内存联动

接下来我们就来完成线程缓存向中心缓存请求内存的接口。

首先我们先来写上层的线程缓存的接口FetchFromCentralCache,我们需要做到以下工作:

  1. 确定申请的个数:采取慢开始反馈调节算法,让请求的内存块逐渐增加,并且要注意有一个上限,这个上限按照申请的大小反比例对应
  2. 向中心缓存进行请求:创建两个变量(开始节点和结束节点),通过接口的输出型参数获取,可以获取到一段内存块链表,注意此时要获取实际的请求个数,因为span中可能不够了。
  3. 返回内存块:将请求获取的若干个内存块中返回一个,其余的储存在对应的自由链表中!!!
//按照字节数反比例返回内存块数量
static size_t SizeClass::NumMoveSize(size_t size)
{
	assert(size < MAX_BYTES);
	if (size == 0) return 0;
	//范围在 [2 , 512]
	int num = MAX_BYTES / size;
	//保证范围
	if (num < 2)
	{
		return 2;
	}
	else if (num > 512)
	{
		return 512;
	}
	else
		return num;

}
void* ThreadCache::FetchFromCentralCache(size_t index, size_t alignSize)
{
	// 慢开始反馈调节算法
	// 1、最开始不会一次向centralcache一次批量要太多,因为要太多了可能用不完
	// 2、如果你不要这个size大小内存需求,那么batchNum就会不断增长,直到上限
	// 3、size越大,一次向central cache要的batchNum就越小
	// 4、size越小,一次向central cache要的batchNum就越大  
	//根据自由链表的Maxsize比较大小 得到这一批的数量
	//如果是批数 == Maxsize , 那Maxsize增加;
	int batchNum = std::min( _freelist[index].MaxSize(), SizeClass::NumMoveSize(alignSize) );
	if (batchNum == _freelist[index].MaxSize())
	{
		//慢调节
		_freelist[index].MaxSize() += 1;
	}
	
	//向中心缓存申请内存
	void* start = nullptr; // 开始节点的地址
	void* end = nullptr;   // 结束节点的地址
	//获取唯一的单例对象进行申请 FetchRangeobj()
	CentralCache* pcc = CentralCache::GetInstance();
	//申请不一定有这么多 , 需要获得实际个数
	size_t actualnum = pcc->FetchRangeobj(start, end, batchNum, alignSize);
	assert(actualnum > 1);
	//如果只有一个直接返回就可以
	if (actualnum == 1)
	{
		assert(start == end);
		return start;
	}
	//有多个就需要把多余的内存块插入到自由链表中  
	else
	{
		_freelist[index].PushRange(NextObj(start), end);
		return start;
	}

}

接下来就来实现中心缓存的发送内存块:

  1. 首先,中心缓存是临界区,找到对应链表后要及时加锁,这里采取RAII形式的锁守卫,方便使用
  2. 然后获取一个span对象,等待与页缓存联动
  3. span对象中的自由链表中进行选取对应数量的内存块,注意不能超出span的范围!!
//给Threadcache提供内存块
size_t CentralCache::FetchRangeobj(void* start, void* end, size_t batchnum, size_t size)
{
	//找到是哪一个桶
	size_t index = SizeClass::Index(size);
	//加减锁
	std::unique_lock<std::mutex> lock(_spanlists[index].GetMutex());

	//获取一个span对象
	Span* span = GetOneSpan(_spanlists[index], size);
	assert(span);
	assert(span->_freelist);
	//对span对象中的链表进行选取
	start = span->_freelist;
	end = start;
	//读取batch个内存块,并且保证不超出范围
	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;
	//RAII规则下的锁守卫会自动释放!
	return actualnum;
}

这样线程缓存的请求内存块的联动就完成了!!!

后续我们来完成页缓存的结构,然后联动起来,将请求内存的联动写好!!!

标签:链表,缓存,span,---,线程,内存,size
From: https://blog.csdn.net/JLX_1/article/details/141758213

相关文章

  • 基于Python的机器学习系列(19):K均值聚类(K-Means Clustering)
    简介        K均值聚类(K-MeansClustering)是一种常用的无监督学习算法,用于将数据样本划分为若干个“簇”,使得同一簇内的数据点彼此相似,而不同簇的数据点之间差异较大。由于K均值不依赖于标签,因此它是一种无监督学习方法。常见的应用包括客户细分、图像分割和数据可视......
  • 嵌入式全栈开发学习笔记---Linux网络编程(面试/开发重点)
    目录网络概述Linux网络基础网络模型TCP/IP协议族体系结构数据封装TCP协议TCP协议头部结构TCP三次握手TCP四次挥手UDP协议UDP协议头部结构套接字Socket端口号和IP地址地址转换字节序转换TCP服务器服务器建立步骤第一步,创建socket--socket()第二步,绑定信息Bin......
  • 心理健康数据可视化系统的设计与实现-计算机毕业设计源码+LW文档
    摘要在信息化时代,心理健康数据日益丰富,但传统的数据处理方式往往难以有效挖掘其深层价值。通过数据可视化,研究人员能够更快速地识别数据中的模式与趋势,为心理健康问题的预防、干预和治疗提供科学依据。同时,对于普通公众而言,数据可视化有助于增强对心理健康问题的认识和理解,促进心......
  • 基于Vue+Django的企业人力资源管理系统设计与实现-计算机毕业设计源码+LW文档
    摘要企业人力资源管理系统是随着信息化时代的到来和人力资源管理理念的更新而应运而生的一种管理工具。在当前竞争激烈的市场环境下,企业面临着人才竞争激烈、管理难度加大等挑战,因此,建立高效、便捷的人力资源管理平台显得尤为重要。企业人力资源管理系统能够大幅提升人力资源管理......
  • first-blog
    各位师傅好,这里是saga131的博客原本打算第一个博客就介绍一下自己,在github上搭建了一个博客,但是遇到了点环境问题,便转战至博客园,用第二个博客来介绍一下自己本人是从2023年10月入门CTF和取证。社团纳新面试中,我选择了密码方向,开始了crypto之路。面试结束后,CPPUISA社团师傅让我了......
  • 操作系统基础——内存管理
    ###内存管理####1.**分页(Paging)**1.**定义**:-分页是一种内存管理技术,将物理内存分成固定大小的块,称为“页框”(PageFrame),同时将逻辑内存分成相同大小的块,称为“页”(Page)。2.**页表(PageTable)**:-页表是一个数据结构,用于存储逻辑地址到物理地址的映射关系。-每......
  • Pulsar 入门实战(3)--安装
    本文主要介绍Pulsar的安装,相关的环境及软件信息如下:CentOS 7.9.2009、Pulsar3.3.0、Java17.0.10。1、单机版安装为了本地开发和测试,可以以单机模式运行Pulsar。单机模式将所有组件运行在单个Java虚拟机(JVM)进程内。官网(https://pulsar.apache.org/download/)下载安装包......
  • 多线程篇(并发编程 - 进程&线程&协程&纤程&管程)(持续更新迭代)
    目录一、进程(Progress)1.进程2.僵尸进程2.1什么是僵尸进程2.2僵尸进程的危害2.3如何避免僵尸进程的产生3.参考链接二、线程(Thread)1.线程是什么?2.多线程2.1.概述2.2.多线程的好处2.3.多线程的代价3.线程模型(三种)3.1.一对一模型3.2.多对一模型3.3......
  • 多线程篇(并发编程 - Java线程实现方式)(持续更新迭代)
    目录一、继承Thread类1.简介2.实现2.1.原始方式2.2.Lambda表达式二、实现Runnable接口1.简介2.实现2.1.原始方式2.2.Lambda表达式三、使用FutureTask1.简介2.实现2.1.原始方式2.2.Lambda表达式四、使用线程池1.ThreadPoolExecutornewCached......
  • 前端css网格布局----行列属性
     固定值方式尽量撑满宽和高三行三列grid-template-rows:200px200px200px;grid-template-columns:200px200px200px;百分比方式四行四列 grid-template-rows:25%25%25%25%;grid-template-columns:25%25%25%25%;repeat(重复几次,数值) 3行3列  g......