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

news2024/9/22 17:37:28

在这里插入图片描述

比起那些用大嗓门企图压制世界的人,
让全世界都安静下来听你小声说话的人更可畏。
--- 韩寒 《告白与告别》---

高并发内存池项目---实现线程缓存

  • 1 框架设计
  • 2 自由链表类和哈希规则
    • 2.1 自由链表类
    • 2.2 映射规则
  • 3 实现线程缓存
    • 3.1 申请内存
    • 3.2 释放内存
  • 4 多线程优化
  • 5 运行测试

1 框架设计

我们需要实现的是一个这样的效果:线程缓存(256KB)中每个空间位置映射到在哈希表上,对应一个自由链表,申请空间时从自由链表中取出一个对象,没有就去中心缓存进行申请!

看起来很容易,但是这一句话之中引出了:

  1. 自由链表 :这需要我们来设计,可以仿照定长池的回收链表来设计。
  2. 哈希映射规则:哈希映射需要很巧妙的进行设计,需要在一个数组中映射出一个大空间中!
  3. 多线程优化:因为项目是针对多线程来进行的优化,所以要保证在多线程情况下可以保证效率!
  4. 线程缓存类:需要可以申请空间,释放空间,空间不足向上申请空间。

所以大致我们需要设计三个类:自由链表类,哈希规则类,线程缓存类。自由链表类和哈希规则类设置为公有类,方便中心缓存和页缓存使用。

//自由链表类
class FreeList
{
public:
	//进行头插
	void Push(void* obj)
	{
	}
	//头删
	void* Pop()
	{
	}
	//判断是否为空
	bool empty()
	{
	}
private:
	//内部是一个指针 指向头结点
	void* _freelist = nullptr;
};

//哈希映射规则类
class SizeClass
{
public:
	//计算对齐数
	static inline size_t RoundUp(size_t size)
	{
	}
	//计算映射的桶下标
	static inline size_t Index(size_t size)
	{
	}
};

class ThreadCache
{
public:
	//申请空间
	void* Allocate(size_t size);
	//释放空间
	void Deallocate(void* ptr, size_t size);
	//向中心缓存申请空间
	void* FetchFromCentralCache(size_t  index, size_t alignSize);
private:
	//哈希映射表
	FreeList _freelist[LISTNUM];
};

2 自由链表类和哈希规则

我们先来实现自由链表类和哈希规则,这两个类都写入到Common.h头文件中,每个文件都可以进行访问!

2.1 自由链表类

自由链表类主要就是插入 和 删除。自由链表中每个节点都有一个指针的空间,可以指向后面的节点。通过这个我们就可以写出插入删除的大致逻辑!

class FreeList
{
private:
	void* NextObj(void* obj)
	{
		return *reinterpret_cast<void**>(obj);
	}
public:
	void Push(void* obj)
	{
		//进行头插
		void* next = NextObj(obj);
		next = _freelist;
		_freelist = obj;
	}
	void* Pop()
	{
		//头删
		void* obj = _freelist;
		void* next = NextObj(_freelist);
		_freelist = next;
		return obj;
	}
	bool empty()
	{
		return _freelist == nullptr;
	}
private:
	//内部是一个指针 指向头结点
	void* _freelist = nullptr;
};

2.2 映射规则

首先我们需要明确如何来进行映射,256KB的空间如果全是8字节对齐的对象,会产生三万多个这可不行!!!
所以需要进行一些特殊处理:并且保证整体最多10%左右的内碎片(由对齐规则导致的内存碎片)浪费:

空间范围对齐规则链表中对应位置个数
[1 , 128]8 byte对齐freelist[0,16)16
[128+1 , 1024]16 byte对齐freelist[16,72)56
[1024+1 , 8*1024]128 byte对齐freelist[72,128)56
[8 * 1024+1 , 64 * 1024]1024 byte对齐freelist[128,184)56
[64 * 1024+1 , 256 *1024]8*1024 byte对齐freelist[184,208)24

我们可以来计算一下内存的浪费率:

  • 申请129字节时 ,按照对齐规则实际需要144字节的空间,那么就是浪费了15字节,
    此时的空间浪费率是 15 / 144 = 0.10。此时是该区间内最坏的情况,

  • 申请8 * 1024 + 1字节时,按照对齐规则需要9216字节的空间,那么就是浪费了1023字节,
    此时空间浪费率是1023 / 9216 = 0.11,此时是该区间内最坏的情况,

所以综合来看,空间浪费率是在10%左右!

按照对齐规则可以将[0 , 256 *1024]的空间大小都能够映射到一个自由链表中去申请对齐后的空间。并且这样只需要208个自由链表,远比30000+少多了奥!!!并且我们还保证了10%左右的空间浪费率!

接下来我们就将这段逻辑写成代码:

  1. RoundUp函数:这个函数是用来计算对齐后需要申请空间的大小,按照我们的几个分类写成若干个if条件判断语句,然后再通过子函数_RoundUp来进行最终的计算。进行对齐的计算为:
    • 如果不能整除就需要向上对齐( 申请空间大小 / 对齐数 + 1)* 对齐数
    • 能整除就直接是申请空间大小
    • 这里有一种非常巧妙的方法:(对齐数 - 1)取反 &(申请空间大小 + 对齐数 - 1)
      原理是对齐数 - 1取反后会得到对齐数对应大小的比特位右边的位置全为0!然后申请空间大小 + 对齐数 - 1会得到向上对齐的最大大小,在取交集,就会得到8的倍数的对齐空间大小了!
  2. Index函数:这个函数是用来找到申请空间大小对应的自由链表!同样也按几个分类写出若干个if条件判断语句,然后通过子函数_Index来进行最终的计算,这个就好理解了,首先先减去前面的不同对齐数的空间大小 ,然后计算在该区间属于第几个自由链表,然后在加上前面不同对齐数的链表数!
class SizeClass
{
private:
	//普通算法
	//static inline size_t _RoundUp(size_t size , size_t alignNum)
	//{
	//	size_t alignsize;
	//	//需要向上对齐
	//	if (size % alignNum != 0)
	//	{
	//		alignsize = (size / alignNum + 1) * alignNum;  24 8
	//	}
	//	//已经对齐
	//	else
	//	{
	//		alignsize = size ;
	//	}
	//	return alignsize;
	//}
	//优秀算法
	static inline size_t _RoundUp(size_t size, size_t alignnum)
	{
		return ~(alignnum - 1) & (size + alignnum - 1);
	}
	static inline size_t _Index(size_t bytes, size_t align_shift)
	{
		//通过字节数返回对应的桶
		//       ( 字节数 + 对齐数 - 1 )/ 对齐数 - 1
		return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;
	}
public:
	static inline size_t RoundUp(size_t size)
	{
		assert(size < MAX_BYTES);
		//通过若干个if语句进行处理
		if (size <= 128)
		{
			//按照8字节对齐
			return _RoundUp(size, 8);
		}
		else if (size <= 1024)
		{
			return _RoundUp(size, 16);
		}
		else if (size <= 8 * 1024)
		{
			return _RoundUp(size, 128);
		}
		else if (size <= 64 * 1024)
		{
			return _RoundUp(size, 1024);
		}
		else if (size <= 256 * 1024)
		{
			return _RoundUp(size, 8*1024);
		}
		else
		{
			assert(false);
			return -1;
		}

	}
	static inline size_t Index(size_t size)
	{
		assert(size < MAX_BYTES);
		int num[5] = { 16 , 56 , 56 , 56 , 24 };
		//通过若干个if语句进行处理
		if (size <= 128)
		{
			//按照8字节对齐
			return _Index(size, 3);
		}
		else if (size <= 1024)
		{
			return _Index(size - 128 , 4) + num[0];
		}
		else if (size <= 8 * 1024)
		{
			return _Index(size - 1024 , 7) + num[0] + num[1];
		}
		else if (size <= 64 * 1024)
		{
			return _Index(size - 8 * 1024, 10) + num[0] + num[1] + num[2];
		}
		else if (size <= 256 * 1024)
		{
			return _Index(size - 64 * 1024, 13) + num[0] + num[1] + num[2] + num[3];
		}
		else
		{
			assert(false);
			return -1;
		}
	}
};

3 实现线程缓存

我们做好了自由链表和哈希规则之后我们来进行线程缓存的实现!

3.1 申请内存

申请内存的逻辑很简单,首先先通过哈希规则得到对齐后的空间大小和对应桶的下标,有了这两个元素我们就可以来进行申请空间!

  1. 该空间大小对应的自由链表中有对象,我们就Pop出来一个就可以了!
  2. 该空间大小对应的自由链表中没有对象,就需要向中心内存进行申请一个对齐后看空间大小的空间!
//申请空间
void* ThreadCache::Allocate(size_t size)
{
	//根据RoundUp计算出对齐后的内存大小
	size_t alignSize = SizeClass::RoundUp(size);
	//根据Index找到对应桶
	size_t index = SizeClass::Index(size);

	//进行取出数据
	if (!_freelist[index].empty())
	{
		return _freelist->Pop();
	}
	else
	{
		//向CentralCache申请内存!
		return FetchFromCentralCache(index, alignSize);
	}
}

3.2 释放内存

释放内存的逻辑更加简单,将需要释放的对象空间直接Push到空间大小对应的自由链表中就可以了

//释放空间
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);

}

这样我们就实现了线程缓存的部分!!!向中心缓存申请的部分还要等到完成中心缓存才可以进行联动!

4 多线程优化

因为我们的项目是要在多线程环境下进行运行,所以要保证线程缓存支持多线程,还要保证线程安全。
现在有个问题:线程缓存是一个类,如何保证每个线程内部都有一个线程缓存!!!
首先肯定是要建立一个全局变量,避免重复构造。但如果只是在主线程中建立一个全局变量,那么就会导致多个线程竞争这个公共资源。那么有没有一种方法可以在线程中建立专属的全局变量,方便进行使用吗、呢?

当然有:TLS( Thread Local Shortage 线程局部存储)无锁访问线程数据。通过这个我们就可以在线程中建立独属于该线程的全局变量。所以我们可以加入一个TSL全局变量;

//TLS( Thread Local Shortage 线程局部存储)无锁访问线程数据
//该写法仅限于VS系列编译器
_declspec(thread) static ThreadCache* pThreadCache = nullptr;

上层进行调用时,先判断pThreadCache是否为空,如果为空那么就创建一个哈希表。反之直接进行使用!这样就避免了使用锁来解决,不需要线程阻塞等待!效率大大提升啊!!!

5 运行测试

为了保证项目的没有BUG,我们要及时进行测试,我们完成了线程缓存,就要保证线程缓存没有问题:
我们先写一下高并发内存池申请内存的接口,将线程缓存使用起来!

// 线程开辟空间函数
// 1. void* ConcurrentAlloc(size_t size)
// 2. void  ConcurrentFree(void* ptr)
// 
void* ConcurrentAlloc(size_t size)
{
	//在该线程中进行内存的申请
	if (pThreadCache == nullptr)
	{
		pThreadCache = new ThreadCache;
	}
	//进行开辟空间
	void * obj = pThreadCache->Allocate(size);
	cout << std::this_thread::get_id() << " : " << pThreadCache << endl;
	return obj;
}
void  ConcurrentFree(void* ptr , size_t size)
{
	assert(ptr);
	//回收空间
	pThreadCache->Deallocate(ptr, size);
}

测试代码:

#include"ObjectPool.h"
#include"ThreadCache.h"
#include"ConcurrentAlloc.h"
#include<windows.h>

//-------- 线程缓存测试 -------
void Alloc1()
{
	for (size_t i = 0; i < 5; i++)
	{
		Sleep(100);
		//申请内存
		ConcurrentAlloc(8);
	}
}
void Alloc2()
{
	for (size_t i = 0; i < 6; i++)
	{
		//申请内存
		ConcurrentAlloc(15);
		Sleep(100);
	}
}
void ThreadCacheTest()
{
	cout << "---- ThreadCacheTest() Begin! ----" << endl;
	std::thread t1(Alloc1);
	std::thread t2(Alloc2);

	t1.join();
	t2.join();

	cout << "---- ThreadCacheTest() Done! ----" << endl;
}

//---------------------------

int main()
{
	//TestObjectPool();
	//ThreadCache tc;
	ThreadCacheTest();
	return 0;
}

运行效果:
在这里插入图片描述
这样就看到每个线程都有对应的资自由链表数组!!!

非常好!!!接下来我们就来实现中心缓存!

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

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

相关文章

day-43 盛最多水的容器

思路 双指针&#xff1a;首先令i0,jheight.length-1,选取短板&#xff08;即Math.min(height[i],height[j])&#xff09;,然后将短板向内移动&#xff0c;直达i>j即可得到答案。 解题过程 短板向内移动&#xff1a;水的容量可能增大 长板向内移动&#xff1a;水的容量不可能…

树莓派5安装系统并配置SSH与VNC权限实现Windows设备远程连接

文章目录 前言1. 使用 Raspberry Pi Imager 安装 Raspberry Pi OS2. Windows安装VNC远程树莓派3. 使用VNC Viewer公网远程访问树莓派3.1 安装Cpolar步骤3.2 配置固定的公网地址3.3 VNC远程连接测试 4. 固定远程连接公网地址4.1 固定TCP地址测试 前言 本文主要介绍如何在树莓派…

数字化转型升级探索(一)

在数字化转型升级的探索中&#xff0c;我们将通过实施综合数字化战略&#xff0c;涵盖从前端用户体验优化到后端系统集成的全方位提升&#xff0c;利用大数据、人工智能、云计算等先进技术对业务流程进行智能化改造&#xff0c;推进自动化和数据驱动决策&#xff0c;推动业务模…

VMware安装Ubuntu 23.10.1系统图文版

文章目录 Ubuntu系统介绍引言Ubuntu系统的特点1. 开源免费2. 易用性3. 稳定性与安全性4. 强大的社区支持 安装与初步设置下载ISO镜像安装1.新建虚拟机2.选择“自定义(高级)”&#xff0c;并点击【下一步】3.选择虚拟机硬件兼容性(默认就好)&#xff0c;并点击【下一步】4.选择“…

爆改yolov8|利用BSAM改进YOLOv8,高效涨点

1&#xff0c;本文介绍 BSAM基于CBAM进行改进&#xff0c;经实测在多个数据集上都有涨点。 BSAM&#xff08;BiLevel Spatial Attention Module&#xff09;是一个用于提升深度学习模型在空间特征处理中的能力的模块。它主要通过双层注意力机制来增强模型对重要空间信息的关注…

一款支持固定区域,固定尺寸大小重复截图的软件

WinSnap是一款功能强大的屏幕截图软件&#xff0c;可以实现对固定区域&#xff0c;固定尺寸大小区域重复截图&#xff0c;适用于日常截图需求和专业用户进行屏幕截图和图像编辑。通过设置快捷键&#xff0c;方便快速重复截图固定区域固定大小。它支持捕捉整个屏幕、活动窗口、选…

H264码流结构讲解

所谓的码流结构就是指&#xff1a;视频经过编码之后所得到的数据是怎样排列的&#xff0c;换句话说&#xff0c;就是编码后的码流我们该如何将一帧一帧的数据分离开来&#xff0c;哪一块数据是一帧图像&#xff0c;哪一块是另外一帧图像&#xff0c;只要了解了这个&#xff0c;…

UE开发中的设计模式(四) —— 组合模式

面试中被面试官问到组合模式和继承有什么区别&#xff0c;给我问懵了&#xff0c;今天又仔细看了下&#xff0c;这不就是UE里的组件吗 >_< 文章目录 问题提出概述问题解决总结组合模式的优缺点继承的优缺点 问题提出 考虑这样一个场景&#xff0c;我们有一个敌人的基类&…

【读书笔记-《30天自制操作系统》-10】Day11

本篇内容继续围绕显示展开。首先对鼠标显示做了些优化&#xff0c;鼠标箭头在到达画面边缘时能够实现部分隐藏&#xff1b;接下来制作了窗口&#xff0c;实现了窗口显示&#xff1b;最后还在窗口的基础上实现了计数器&#xff0c;显示计数的变化并消除闪烁的问题。 1. 画面边…

Java 6.3 - 定时任务

为什么需要定时任务&#xff1f; 常见业务场景&#xff1a; 1、支付10min失效 2、某系统凌晨进行数据备份 3、视频网站定时发布视频 4、媒体聚合平台每10min抓取某网站数据为己用 …… 这些场景需要我们在某个特定时间去做某些事情。 单机定时任务技术有哪些&#xff1f…

form-data和x-www-form-urlencoded的区别

form-data和x-www-form-urlencoded区别 form-data 和 x-www-form-urlencoded 都是表单请求的一种格式&#xff0c;主要区别有两点。 编码方式不同 我们先说x-www-form-urlencoded&#xff0c;它的编码方式就隐藏在名字里&#xff1a;urlencoded。看到这个关键词&#xff0c;…

Gradle下载失败或者慢怎么办

在Android Studio开发过程中&#xff0c;经常需要下载Gradle构建工具来构建项目。然而&#xff0c;由于网络限制或国际镜像服务器响应慢&#xff0c;Gradle的下载过程可能会非常缓慢甚至失败。为了优化这一过程&#xff0c;我们可以采用国内的Gradle镜像来加速下载。同时&#…

期权的交易时间是什么时候?期权具体交易时间分享!

今天带你了解期权的交易时间是什么时候&#xff1f;期权具体交易时间分享&#xff01;期权交易时间与股票市场同步&#xff0c;每个交易日的上午9:15至11:30和下午13:00至15:00为主要的交易时间。 50ETF期权交易时间 1.50ETF期权交割日&#xff1a; 固定的规则&#xff1a; …

FLUX 1 将像 Stable Diffusion 一样完整支持ControlNet组件

之前 InstantX 团队做的多合一的 Flux ControlNet 现在开始和 ShakkerAI 合作并推出了&#xff1a;Shakker-Labs/FLUX.1-dev-ControlNet-Union-Pro 该模型支持 7 种控制模式&#xff0c;包括 canny (0), tile (1), depth (2), blur (3), pose (4), gray (5) 和 low quality (6)…

Priority_Queue 的使用和模拟

目录 一基本的介绍 优先队列是一种容器适配器&#xff1b;他的第一个元素总是他包含所有元素里面最大的一个。 他的底层容器可以是任何标准容器类模板&#xff0c;也可以是其他特定设计的容器类。 这个底层容器应该可以通过随机访问迭 代器&#xff0c;并支持以下操作&#x…

【LINUX】ifconfig -a查看到的发送、接收包数和字数字节数在驱动层代码大概位置

先看结果 ifconfig -a查看到发送的信息&#xff1a; 从上图可以看出来网卡驱动代码的目录是在drivers/net/ethernet/intel/e1000/e1000_main.c 下图是接收到的信息&#xff1a; 不过这些数据是在虚拟机看到的&#xff0c;如果有条件可以在实际的物理网卡测试看看效果。 下边这…

IO进程线程 0828作业

作业 有名管道&#xff0c;创建两个发送接收端&#xff0c;父进程写入管道1和管道2&#xff0c;子进程读取管道2和管道1 mkfifo1.c代码 #include <myhead.h> int main(int argc, const char *argv[]) {if(mkfifo("./my_fifo1",0664) -1){perror("mkfi…

轻量级数据库

在计算机编程中&#xff0c;句柄&#xff08;Handle&#xff09;是一种用于标识和引用系统内部对象的标识符。句柄通常是一个不透明的指针或整数值&#xff0c;它代表了一个系统资源&#xff0c;如文件、窗口、进程、线程、内存映射文件等。句柄的主要作用是在程序中引用这些资…

【Docker】搭建docker的私有仓库

一、为什么搭建私有仓库 docker hub虽然方便&#xff0c;但是还是有限制 需要internet连接&#xff0c;速度慢所有人都可以访问由于安全原因企业不允许将镜像放到外网 好消息是docker公司已经将registry开源&#xff0c;我们可以快速构建企业私有仓库 二、搭建简单的Registr…