首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >高并发内存池(一):项目简介+定长内存池的实现

高并发内存池(一):项目简介+定长内存池的实现

作者头像
用户11719958
发布2025-12-30 11:59:23
发布2025-12-30 11:59:23
1100
举报

一,项目介绍

这个项目是实现一个高并发的内存池。它的原型是google的一个开源醒目tcmalloc,tcmalloc全称Thread-Caching Malloc,即线程缓存的malloc,实现了高效的线程内存管理,用于替代系统的相关内存分配函数(malloc,free)。

这个项目是对tcmalloc的一个简化版,是对tcmalloc的学习,实现一个我们自己的高并发内存池。

在该项目中涉及到的C++知识包括数据结构(链表,哈希桶),操作系统内存管理,单例模式,多线程 以及互斥锁等等 。

二,什么是内存池

1,池化技术

所谓池化技术,就是程序先向系统申请过量的资源,然后自己管理,以备不时之需。之所以要申请过量的资源,是因为每次向操作系统申请资源时,都有较大的开销。如果我们提前申请好一块大的空间,再次申请时,直接使用就会变得非常快捷,大大提高程序运行效率。

在计算机中,有很多使用"池"这种技术的地方。除了内存池,还有连接池,线程池,对象池等等 。以线程池为例,它的主要思想是:先启动若干线程,让他们进入休眠状态,当接收到客户端发来的任务请求时,唤醒其中的某个线程,让它来处理客户端的任务请求,当 处理完这个请求,不是终止掉该线程,而是让该线程继续进入休眠状态。

2,内存池

内存池是指程序预先从操作系统申请一大块的内存。此后,当程序需要再次申请内存时,不是直接向操作系统申请,而是直接从内存池中获取;同理,当程序释放内存的时候,不是直接还给操作系统,而是还给内存池。当程序退出(或特定时间)时,内存池才将之前申请的内存真正释放,还给操作系统 。

3,内存池主要解决的问题

内存池的设计就是为了解决频繁向操作系统申请资源的问题,从而提高程序运行的效率。

我们将内存池看作是系统的内存分配器,就还面临一个问题:内存碎片问题。内存碎片又分为内碎片和外碎片。

所谓外碎片问题,就是我们的程序先后申请了一大块内存之后,在释放内存的时候,有的释放了,有的还在使用,导致归还的内存不连续,如果下次再次申请内存的时候,可能会由于内存地址不连续的问题,导致申请内存失败。如下图:

对于内碎片,在后面的项目中会遇到,到时再讲解。

4,malloc

C/C++中,我们要动态申请内存都是通过malloc的,但是实际上,我们不是直接取堆上要内存的。

因为malloc本身就是一个内存池。malloc相当于向操作系统“批发”要了一大块的内存空间,然后“零售”给程序使用。当全部“售完”或者有大量的内存需求时,再根据实际需要向操作系统“进货”。

但是malloc在多线程环境下,内存申请效率不可观,这也是我们所实现的高并发内存池和malloc的重要区别。

如下图所示:

三,实现一个定长内存池

所谓定长内存池,顾名思义,它就是用来解决固定内存大小的申请。而我们的高并发内存池实现的是任意大小的内存申请。


为什么要实现一个定长内存池? 首先,定长内存池相对于高并发内存池来说,实现起来简单。同时定长内存池这里的实现设计思想,在后面的项目中也会使用到。

定长内存池的设计

有了对内存池的理解之后,这里的实现就没有什么太大的问题。

定长内存池,解决固定大小的内存申请,假设大小为n。

  • 申请空间时

我们首先需要一个变量来记录这一大块内存的起始地址。我们可以定义一个char* memory的类型来记录这一大块内存的起始地址。使用char*来表示的好处是,char是一个字节大小,当申请n个字节的内存大小时,我们只需让memory向后走n个字节大小即可。不能使用void*,因为void*不能解引用,不能++。

  • 释放空间时

由于在申请资源时,有的对象释放空间,有的还正在使用。所以这就有可能导致还回来的内存不一定是连续的。例如:本来连续的内存空间是1,2,3,4,5,还回来的时候可能是3,1,5。 所以我们不能再将这些内存资源让memory管理起来。

  • 对于这些还回来的空间,我们可以将这些空间以链表的形式组织起来。按照以往的思路,我们会定义一个结构体,结构体中包含还回来的空间,以及一个next指针指向下一个还回来的结构体。
  • 其实不需要我们自己定义一个结构体对象。对于这些还回来的内存,我们可以让它前4个字节(或者8个字节)存储下一个还回来的内存的地址。32位环境下是前4个字节,64位下就是前8个字节。具体看下图:

这样,我们就可以将还回来的内存以链表的组织起来,我们定义一个自由链表 void* freelist,来指向第一个还回来的内存的地址。

大致结构

代码语言:javascript
复制
//定长内存池
//定长大小就是T的类型大小
template <class T>
class FixedMemoryPool
{
public:
	//...
private:
	char* _memory = nullptr;//大块内存的起始地址
	void* _freelist = nullptr;//自由链表,管理还回来的内存
    size_t _remainSize = 0;//记录当前内存池剩余的内存大小
};

核心功能实现

申请一块大小为T的内存

申请内存时,刚开始时整个内存池的大小是空的,此时我们需要向操作系统申请。

并且在分配内存过程中,如果剩余内存的大小,小于1个T类型的大小时,也需要向操作系统再次申请。这两种情况可以放在一起解决。

代码语言:javascript
复制
T* New()
{
	T* obj = nullptr;//申请的T对象

	//当剩余的空间大小不够一个对象的大小时,重新向操作系统申请一大块内存
	if (_remainSize <sizeof(T))
	{
		_remainSize = 128 * 1024;//申请128KB
		_memory = (char*)malloc(_remainSize);
		if (_memory == nullptr)//申请内存失败
		{
			throw::bad_alloc();
		}
	}
	obj = (T*)_memory;
	_memory += sizeof(T);
	_remainSize -= sizeof(T);
	return obj;
}
释放一块大小为T的内存

现在考虑两个问题: 如何将还回来的内存块以链表的形式组织在一起??

  • 对于还回来的两个对象obj1和obj2,我们现在要构成链表:obj1->obj2。我们可以先将obj1转换成int*类型,然后再解引用,就可以访问到obj1的前4个字节了。
  • 即*(int*)obj1=obj2。这样就实现了让obj1的前4个字节存储上obj2的地址,这样就构建成了一条链表。

如何解决不同平台的问题,如果是32位机器,指针是4个字节大小。但如果是64位机器,指针式8个字节大小,上面的代码就跑不动了???

  • 其实只需要改动 一点即可了:*(int**)obj1=obj2;这里不一定是int**,也可以换成char**,long long**,void**,只要是二级指针就可以。
  • 我们之前是将obj1强转成int类型,解引用后就可以访问4个字节的大小。此时我们将ob1强转成int**类型的,那么解引用后访问的内存大小就是int*大小的,而int*是一个指针,它在32位机器下是4字节,在64位环境下是8字节,所以解引用后就可以访问到4个字节(或8个字节),这样就可以解决不同环境 的问题了。

然后实现释放内存的逻辑,使用自由链表将对应的内存块以链表的形式组织在一起。

将还回来的内存空间以链表的形式组织起来,让_freelist指向该链表的头节点。然后,每当释放一块内存时,将该内存块 头插到链表中 。

当有一块内存还回来时:

  • 当自由链表为空时,我们只需让_freelist指向该内存,并让该内存块的前4个字节(或8个字节)填充上nullptr即可。此时_freelist指向该内存块,下一个内存块的地址为空。
  • 当自由链表不为空时,我们先将该内存块的前4个字节(或8个字节)填充上_freelist的next地址,然后再让_freelist的next填充上当前内存块的地址即可
  • 其实这两种情况下的头插时一样的,代码实现时一样的。
代码语言:javascript
复制
	void Delete(T* obj)
	{
		//使用自由链表管理起来
		obj->~T();

		//头插
		*(void**)obj = _freelist;
		_freelist = obj;
	}
完整的申请内存过程

有了对上面释放内存块的理解,其实就是通过自由链表来维护这些还回来的内存块。

那么对于申请内存模块,还需要补充一点。就是在申请内存时,如果自由链表不为空,优先从自由链表中获取,这是一个头删的过程。让自由链表_freelist向后移动,指向下一个内存块。

还需要处理一个问题,就是如果我们申请的内存块大小不够4个字节(或8个字节)时,在管理还回来的内存时,内存块的大小不足4个字节(或8个字节),那么就无法存放地址。所以,为了解决这个问题,我们在申请内存时,如果所申请的内存大小不足4个字节(或者8个字节),就给它4个字节(或8个字节),因为至少是需要4个字节(或8个字节)的。

代码语言:javascript
复制
	T* New()
	{
		T* obj = nullptr;//申请的T对象

		//优先使用还回来的对象
		if (_freelist)
		{
			//自由链表头删过程
			void* next = *(void**)_freelist;//保存下一个内存块的地址
			_obj = (T*)_freelist;
			_freelist = next;//指向下一个节点,完成头删
		}
		else
		{
			//当剩余的空间大小不够一个对象的大小时,重新向操作系统申请一大块内存
			if (_remainSize < sizeof(T))
			{
				_remainSize = 128 * 1024;//申请128KB
				_memory = (char*)malloc(_remainSize);
				if (_memory == nullptr)//申请内存失败
				{
					throw::bad_alloc();
				}
			}
			obj = (T*)_memory;
			//不足4个字节或8个字节时,就给4个字节或者8个字节大小
			size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
			_memory += objSize)
			_remainSize -= objSize;
		}
		//使用定位new初始化
		new(obj)T;

		return obj;
	}

测试代码

这里以二叉树节点为例,比较标准库中的new和我们实现的内存池的性能

代码语言:javascript
复制
struct TreeNode
{
	int _val;
	TreeNode* _left;
	TreeNode* _right;

	TreeNode()
		:_val(0)
		, _left(nullptr)
		, _right(nullptr)
	{}
};

void TestFiedMemoryPool()
{
	// 申请释放的轮次
	const size_t Rounds = 5;

	// 每轮申请释放多少次
	const size_t N = 100000;

	std::vector<TreeNode*> v1;
	v1.reserve(N);

	size_t begin1 = clock();
	for (size_t j = 0; j < Rounds; ++j)
	{
		for (int i = 0; i < N; ++i)
		{
			v1.push_back(new TreeNode);
		}
		for (int i = 0; i < N; ++i)
		{
			delete v1[i];
		}
		v1.clear();
	}

	size_t end1 = clock();

	std::vector<TreeNode*> v2;
	v2.reserve(N);

	FixedMemoryPool<TreeNode> TNPool;
	size_t begin2 = clock();
	for (size_t j = 0; j < Rounds; ++j)
	{
		for (int i = 0; i < N; ++i)
		{
			v2.push_back(TNPool.New());
		}
		for (int i = 0; i < N; ++i)
		{
			TNPool.Delete(v2[i]);
		}
		v2.clear();
	}
	size_t end2 = clock();

	cout << "new cost time:" << end1 - begin1 << endl;
	cout << "object pool cost time:" << end2 - begin2 << endl;
}

可以看到,我们实现的定长内存池比标准库中的申请内存的方法更加高效。

源码: ConcurrentMemoryPool · 小鬼/高并发内存池 - 码云 - 开源中国

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-12-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一,项目介绍
  • 二,什么是内存池
    • 1,池化技术
    • 2,内存池
    • 3,内存池主要解决的问题
    • 4,malloc
  • 三,实现一个定长内存池
    • 定长内存池的设计
    • 大致结构
    • 核心功能实现
      • 申请一块大小为T的内存
      • 释放一块大小为T的内存
      • 完整的申请内存过程
    • 测试代码
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档