七、高并发内存池--Page Cache

七、高并发内存池–Page Cache

7.1 PageCache的工作原理

PageCache是以span的大小(以页为单位)和下标一一对应为映射关系的哈希桶,下标是几就说明这个哈希桶下挂的span的大小就是几页的,是绝对映射的关系。因为PageCache也是全局只有唯一一个的,所以为了防止出现线程安全的问题,在访问PageCache之前要先加上一把大锁锁住整个PageCache,为什么这里是加一把大锁锁住整个PageCache而不是加桶锁锁住要访问的那个桶呢?

因为当Central Cache向PageCache获取一个K页的span的时候,在PageCache中下标为K的哈希桶中不一定有span,此时PageCache并不是直接向系统堆中申请一块K页的span内存,因为如果这样处理的话会出现频繁地向系统申请内存的情况,并且每次向堆申请的内存都只是K页大小的,如果每一次的K都是1或者2,这样会使堆申请出来的内存都是很碎片化的,不便于堆做内存管理,所以PageCache的处理策略是如果当前下标的哈希桶中没有span对象,那么就从当前位置开始往后遍历,如果后面的哈希桶中有span对象就把这个span切成一个K页的kspan和一个(n-k)页的span,把kspan切成一个一个的小对象连接到kspan中的自由链表并把这个切好的kspan返回给CentralCache对应位置的哈希桶,剩下的(n-k)页大小的span就挂到PageCache中n-k位置的哈希桶。

如果把PageCache中剩下的所有的哈希桶都遍历完都没有找到一个span,那么就向系统申请一个128(这个128是自定义的)页大小的大span,重复上面的切分操作,最后返回一个切好的K页的span给CentralCache对应位置的哈希桶中,再让CentralCache分若干个小对象给ThreadCache,ThreadCache再返回一个给上层。

综上,也就不难发现PageCache不能用桶锁而是要用一把大锁,因为在CentralCache向PageCache中获取一个span时不仅仅是会访问PageCache中的某一个位置的哈希桶,而是有可能往后遍历其它的哈希桶并访问,所以加桶锁只是锁住了一个桶,并不能锁住后续遍历的其它位置的哈希桶,所以这里要加一把大锁。其实从技术上来说用桶锁也是可以的,只不过在遍历PageCache后面的每一个哈希桶前都要对先对这个桶加上桶锁,访问完之后还要解掉桶锁,如此一来,必然会出现大量的上锁解锁的操作,反而会降低了内存池的效率,所以加一把大锁就行了。

在这里插入图片描述

7.2 PageCache.h

//页缓存,CentralCache需要向PageCache申请K页大小的span,PageCache向系统申请128页大小的span
//因为PageCache也是全局只有唯一一个的,所以也把PageCache设计成单例模式
class PageCache
{
public:
	static PageCache* GetInstance()
	{
		return &_sInst;
	}

	//提供给CentralCache向PageCache获取一个k页的span时使用
	Span* NewSpan(size_t k);

	//根据obj的地址转换成页号,进而通过哈希表找到页号对应的Span
	Span* MapObjectToSpan(void* obj);

	//CentralCache把span还回来给PageCache
	void ReleaseSpanToPageCache(Span* span);

	//PageCache是整个进程唯一的,所以所有的线程都有可能
	//访问PageCache,存在线程安全问题,所以需要加锁,这里是加一把大锁而不是桶锁
	std::mutex _pageMtx;

private:
	//单例模式需要把构造函数私有化,防止别人创建对象
	PageCache()
	{}

	PageCache(const PageCache&) = delete;

private:
	SpanList _spanLists[NPAGES];//有多少也大小的span数组就有多大,下标和span的大小一一对应

	//声明静态对象
	static PageCache _sInst;

	//如果PageCache中所有的哈希桶都没有span,需要从系统堆中申请内存时
	//要脱离malloc,所以直接用我们写好的定长内存池申请内存即可,定长内存
	//池中封装了系统调用接口,直接向堆申请内存和释放内存
	ObjectPool<Span> _spanPool;

	//保存页号和span的映射关系,为了后续能找到每一个还回来的小对象属于哪一个span
	std::unordered_map<PAGE_ID, Span*> _idSpanMap;

};

7.3 PageCache.cpp


//静态对象需要在类内声明,类外面定义
PageCache PageCache::_sInst;

//k代表的是这个span的大小k页
Span* PageCache::NewSpan(size_t k)
{
	assert(k > 0);

	//如果申请的span的大小大于128页,则需要直接向堆申请
	//因为PageCache只有128个有效的哈希桶
	if (k > NPAGES - 1)
	{
		//向堆申请k页内存
		void* ptr = SystemAlloc(k);
		Span* kSpan = _spanPool.New();//_spanPoll是定长内存池对象,直接向堆申请内存
		//地址转化成页号
		kSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
		kSpan->_n = k;
		//把页号和Kspan的映射关系放进Map中
		_idSpanMap[kSpan->_pageId] = kSpan;
		return kSpan;
	}
	else
	{
		//如果PageCache第k个位置的哈希桶上有k页大小的span,则直接返回一个span
		if (!_spanLists[k].Empty())
		{
			Span* kSpan = _spanLists[k].PopFront();

			//把kSpan的页号和对应的Span*的映射关系存放到哈希桶中去,方便
			//CentralCache回收小块内存时,查找对应的span
			//kSpan代表的是一个k页大小的Span的大块内存,kSpan->_pageId
			//代表这个大块内存的起始地址,有k页,所以这k页映射到的都是这个Span
			// 
			//这里曾经没有存映射关系,导致归还小内存块的时候找不到所属的span(细节)
			for (PAGE_ID i = 0; i < kSpan->_n; i++)
			{
				_idSpanMap[kSpan->_pageId + i] = kSpan;
			}

			return kSpan;
		}

		//走到这里说明PageCache第k个位置的哈希桶没有k页大小的span,则需要遍历
		//后面的大于k页的哈希桶,找到了一个n页大小的span就把这个span切分成一个
		// k页大小的span和一个n-k页大小的span,k页的返回,n-k页的挂到对应的哈希桶中

		//遍历后面的哈希桶
		for (size_t i = k + 1; i < NPAGES; i++)
		{
			//找到了一个不为空的i页的哈希桶,就对它进行切分
			if (!_spanLists[i].Empty())
			{
				//k页的span
				Span* kSpan = _spanPool.New();
				//n页的span
				Span* nSpan = _spanLists[i].PopFront();//把这个i页大小的span拿出来进行切分

				//开始把一个n页的span切分成一个k页的span和一个n-k页的span
				// 
				//从nSpan的头上切k页给kSpan,所以kSpan的页号就是nSpan的页号
				kSpan->_pageId = nSpan->_pageId;
				kSpan->_n = k;//kSpan的页数是k

				//被切分以后nSpan的页号需要+=k页,因为头nSpan的头k页已经切分给了kSpan
				nSpan->_pageId += k;
				nSpan->_n -= k;//nSpan的页数要-=k页,因为nSpan被切走了k页

				//把kSpan的页号和对应的Span*的映射关系存放到哈希桶中去,方便
				// CentralCache回收小块内存时,查找对应的span
				//kSpan代表的是一个k页大小的Span的大块内存,kSpan->_pageId
				//代表这个大块内存的起始地址,有k页,所以这k页映射到的都是这个Span
				for (PAGE_ID i = 0; i < kSpan->_n; i++)
				{
					_idSpanMap[kSpan->_pageId + i] = kSpan;
				}

				//nSpan被切分后的首页和尾页的页号和nspan的映射关系也需要保存起来
				//以便后续合并,因为合并的方式是前后页合并,往前找肯定找到的是一个span的
				//最后一页,往后找一定找的是一个span的第一页,所以挂在PageCache对应哈希桶
				//的span的第一页和最后一页与span的关系也需要保存起来
				_idSpanMap[nSpan->_pageId] = nSpan;
				_idSpanMap[nSpan->_pageId + nSpan->_n - 1] = nSpan;


				//把剩余的n-k页的span头插到对应下标的哈希桶中,nSpan->_n已经是改过的了,不用再-k
				_spanLists[nSpan->_n].PushFront(nSpan);
				return kSpan;
			}
		}

		//走到这里说明前面的NPAGES个哈希桶中都没有Span,(例如第一次申请内存时)
		//则需要向堆申请一个128页大小的span大块内存,挂到对应的哈希桶中
		void* ptr = SystemAlloc(NPAGES - 1);

		Span* bigSpan = _spanPool.New();
		bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;//内存的地址需要转换成页号映射到对应的哈希桶中
		bigSpan->_n = NPAGES - 1;//大块内存的页数,描述这个bigspan的大小

		//把NPAGES-1页大小的span头插到对应NPAGES-1号桶中去
		_spanLists[bigSpan->_n].PushFront(bigSpan);

		//本质是运用了复用的设计,避免代码中出现重复的逻辑
		return NewSpan(k);
	}

}

Span* PageCache::MapObjectToSpan(void* obj)
{
	//计算出obj对应的页号
	PAGE_ID id = (PAGE_ID)obj >> PAGE_SHIFT;

	//访问_idSpanMap的时候需要加锁,避免线程安全的问题
	//这里使用C++11的RAII锁,出了这个函数这把锁会自动解掉
	std::unique_lock<std::mutex> lock(_pageMtx);

	//通过页号查找该内存块对应的是哪一个span
	auto ret = _idSpanMap.find(id);
	if (ret != _idSpanMap.end())
	{
		return ret->second;
	}
	else
	{
		assert(false);
		return nullptr;
	}
}

//CentralCache把span还回来给PageCache
void PageCache::ReleaseSpanToPageCache(Span* span)
{
	//如果span的页数大于128页,则说明这个span是从堆上直接申请的,
	//直接释放给堆即可,不能挂到PageCache的哈希桶中,因为PageCache一个只有128个桶
	if (span->_n > NPAGES - 1)
	{
		void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
		SystemFree(ptr);
		_spanPool.Delete(span);
		return;
	}

	while (1)
	{
		//找前一页的span,看是否能够和当前页合并,如果能,则循环向前合并,直到不能合并为止
		PAGE_ID prevId = span->_pageId - 1;
		auto ret = _idSpanMap.find(prevId);

		//_idSpanMap中没找到前一页和对应span,说明前一页的内存没有被申请,结束合并
		if (ret == _idSpanMap.end())
		{
			break;
		}

		Span* prevSpan = ret->second;
		//如果前一页对应的span在CentralCache中正在被使用,结束合并
		if (prevSpan->_isUse == true)
		{
			break;
		}

		//如果和前一页合并之后会超过哈希桶的最大的映射返回,结束合并
		if (prevSpan->_n + span->_n > NPAGES - 1)
		{
			break;
		}

		//合并span和prevSpan
		span->_pageId = prevSpan->_pageId;
		span->_n = prevSpan->_n + span->_n;
		//合并之后需要把prevSpan在对应的哈希桶中删除掉
		_spanLists[prevSpan->_n].Erase(prevSpan);
		//因为prevSpan已经被合并到了span中,所以prevSpan对应的内存可以delete掉了
		_spanPool.Delete(prevSpan);

	}

	while (1)
	{
		//找span的下一个span的起始页号
		PAGE_ID nextId = span->_pageId + span->_n;
		auto ret = _idSpanMap.find(nextId);
		if (ret == _idSpanMap.end())
		{
			break;
		}

		Span* nextSpan = ret->second;
		if (nextSpan->_isUse == true)
		{
			break;
		}

		if (nextSpan->_n + span->_n > NPAGES - 1)//曾经写成NPAGES+1了
		{
			break;
		}

		//span的起始页号不变,页数相加
		span->_n = span->_n + nextSpan->_n;
		//合并之后需要把prevSpan在对应的哈希桶中删除掉
		_spanLists[nextSpan->_n].Erase(nextSpan);
		_spanPool.Delete(nextSpan);
	}

	//合并得到的新的span需要挂到对应页数的哈希桶中
	_spanLists[span->_n].PushFront(span);
	//在PageCache中的span要设置为false,好让后面相邻的span来合并
	span->_isUse = false;
	//为了方便后续的合并,需要把span的起始页号和尾页号和span建立映射关系
	_idSpanMap[span->_pageId] = span;
	_idSpanMap[span->_pageId + span->_n - 1] = span;

}

以上就是高并发内存池三层模型的核心部分,后面的文章是对该高并发内存池的性能测试以及针对性能的瓶颈区做出相应的优化策略。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

线上批量查询物流导出到表格的操作指南

现在的生活中&#xff0c;我们经常需要查询包裹物流信息。如果一次性需要查询多个快递单号的物流信息&#xff0c;手动一个一个查询会非常麻烦。今天&#xff0c;我将向大家分享一个简单实用的方法&#xff0c;可以批量查询物流并导出到表格&#xff0c;方便随时查看。 首先&am…

js 正则表达式 验证 :页面中一个输入框,可输入1个或多个vid/pid,使用英文逗号隔开...

就是意思一个输入框里面&#xff0c;按VID/PID格式输入,VID和PID最大长度是4,最多50组 1、页面代码 <el-form ref"ruleForm" :model"tempSet" :rules"rules" label-position"right"> <!-- 最多 50组&#xff0c;每组9个字符…

【USRP】集成化仪器系列1 :信号源,基于labview实现

USRP 信号源 1、设备IP地址&#xff1a;默认为192.168.10.2&#xff0c;请勿 修改&#xff0c;运行阶段无法修改。 2、天线输出端口是TX1&#xff0c;请勿修改。 3、通道&#xff1a;0 对应RF A、1 对应 RF B&#xff0c;运行 阶段无法修改。 4、中心频率&#xff1a;当需要…

LinuxUbuntu安装OpenWAF

Linux&Ubuntu安装OpenWAF 官方GitHub地址 介绍 OpenWAF&#xff08;Web Application Firewall&#xff09;是一个开源的Web应用防火墙&#xff0c;用于保护Web应用程序免受各种网络攻击。它通过与Web服务器集成&#xff0c;监控和过滤对Web应用程序的流量&#xff0c;识…

JDK源码解析-LinkedList

1. LinkedList类 1.1 LinkedList类定义&数据结构 定义 LinkedList是一种可以在任何位置进行高效地插入和移除操作的有序序列&#xff0c;它是基于双向链表实现的。 数据结构 基础知识补充 单向链表&#xff1a; element&#xff1a;用来存放元素 next&#xff1a;用来…

Redis 7 第六讲 主从模式(replica)

🌹🌹🌹 此篇开始进入高级篇范围(❤艸`❤) 理论 即主从复制,master以写为主,Slave以读为主。当master数据变化的时候,自动将新的数据异步同步到其它slave数据库。 使用场景 读写分离 容灾备份数据备份水平扩容主从架构 演示案例 注:masterauth、replicaof主…

FTP文件传输服务器

目录 一、FTP协议两种工作模式 二、FTP数据两种传输模式 三、FTP用户分类 四、VSFTP配置案例 4.1匿名开放模式 4.2本地用户模式 4.3虚拟用户模式 五、实验总结 一、FTP协议两种工作模式 主动模式&#xff1a; 1、客户端主动向ftp服务器发送控制连接&#xff0c;三次握手控制连接…

go锁-互斥锁

go锁-互斥锁 sema初始值是0&#xff0c;waitershift等待协程的数量 正常枷锁&#xff1a; 尝试CAS直接加锁&#xff0c;通过原子包给lockerd 为枷锁 若无法直接获取&#xff0c;进行多次自旋尝试&#xff0c;未获取到的锁的g &#xff0c;多次执行空语句&#xff0c;多次尝试…

线程同步与互斥

目录 前言&#xff1a;基于多线程不安全并行抢票 一、线程互斥锁 mutex 1.1 加锁解锁处理多线程并发 1.2 如何看待锁 1.3 如何理解加锁解锁的本质 1.4 CRAII方格设计封装锁 前言&#xff1a;基于线程安全的不合理竞争资源 二、线程同步 1.1 线程同步处理抢票 1.2 如何…

《QDebug 2023年8月》

一、Qt Widgets 问题交流 1.获取 QWidget 当前所在屏幕区域 本来以为 QWidget 的 screen() 接口返回的是组件自己所在屏幕的 QSreen&#xff0c;实测是所属 Window 所在的屏幕&#xff0c;如果 Window 跨屏了两者所属屏幕可能就不是同一个。 获取 QWidget 当前所在屏幕区域可…

HTTP与SOCKS5的区别对比

在互联网世界中&#xff0c;服务器是一种重要的工具&#xff0c;可以帮助我们提高网络安全性等。今天&#xff0c;我们将重点关注两种常见的技术&#xff1a;HTTP和SOCKS5。让我们深入了解它们的工作原理、用途和优缺点&#xff0c;并通过Python代码示例学习如何使用它们。 HT…

Uniapp笔记(五)uniapp语法4

本章目标 授权登录【难点、重点】 条件编译【理解】 小程序分包【理解】 一、授权登录 我的模块其实是两个组件&#xff0c;一个是登录组件&#xff0c;一个是用户信息组件&#xff0c;根据用户的登录状态判断是否要显示那个组件 1、登录的基本布局 <template><…

QT创建可移动点类

效果如图所示&#xff1a; 创建新类MovablePoint&#xff0c;继承自QWidget. MovablePoint头文件: #ifndef MOVABLEPOINT_H #define MOVABLEPOINT_H#include <QWidget> #include <QPainter> #include <QPaintEvent> #include <QStyleOption> #includ…

2023年7月京东打印机行业品牌销售排行榜(京东运营数据分析)

鲸参谋监测的京东平台7月份打印机行业销售数据已出炉&#xff01; 7月份&#xff0c;打印机市场呈现下滑趋势。根据鲸参谋平台的数据可知&#xff0c;当月京东平台打印机的销量为48万&#xff0c;环比下降约28%&#xff0c;同比下降约18%&#xff1b;销售额为4亿&#xff0c;环…

OCR多语言识别模型构建资料收集

OCR多语言识别模型构建 构建多语言识别模型方案 合合&#xff0c;百度&#xff0c;腾讯&#xff0c;阿里这四家的不错 调研多家&#xff0c;发现有两种方案&#xff0c;但是大多数厂商都是将多语言放在一个字典里&#xff0c;构建1w~2W的字典&#xff0c;训练一个可识别多种语…

Java 8 新特性——Lambda 表达式(2)

一、Java Stream API Java Stream函数式编程接口最初在Java 8中引入&#xff0c;并且与 lambda 一起成为Java开发里程碑式的功能特性&#xff0c;它极大的方便了开放人员处理集合类数据的效率。 Java Stream就是一个数据流经的管道&#xff0c;并且在管道中对数据进行操作&…

IP初学习

1.IP报文 首部长度指的是报头长度&#xff0c;用于分离报头和有效载荷 2.网段划分 IP地址 目标网络 目标主机 3.例子 4.特殊的IP地址 5.真正的网络环境 6.调制解调器 “猫”&#xff0c;学名叫宽带无线猫 7.NAT 源IP在内网环境不断被替换 8.私有IP不能出现在公网上 因…

时序预测 | MATLAB实现CNN-BiGRU卷积双向门控循环单元时间序列预测

时序预测 | MATLAB实现CNN-BiGRU卷积双向门控循环单元时间序列预测 目录 时序预测 | MATLAB实现CNN-BiGRU卷积双向门控循环单元时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.MATLAB实现CNN-BiGRU卷积双向门控循环单元时间序列预测&#xff1b; 2.运行环境…

性能提升3-4倍!贝壳基于Flink + OceanBase的实时维表服务

作者介绍&#xff1a;肖赞&#xff0c;贝壳找房&#xff08;北京&#xff09;科技有限公司 OLAP 平台负责人&#xff0c;基础研发线大数据平台部架构师。 贝壳找房是中国最大的居住服务平台。作为居住产业数字化服务平台&#xff0c;贝壳致力于推进居住服务的产业数字化、智能…

嵌入式学习笔记(1)ARM的编程模式和7种工作模式

ARM提供的指令集 ARM态-ARM指令集&#xff08;32-bit&#xff09; Thumb态-Thumb指令集&#xff08;16-bit&#xff09; Thumb2态-Thumb2指令集&#xff08;16 & 32 bit&#xff09; Thumb指令集是对ARM指令集的一个子集重新编码得到的&#xff0c;指令长度为16位。通常在…