【项目实战】高并发内存池

目录

项目介绍

1.这个项目做的是什么?

2.这个项目的要求的知识储备和难度?

内存池的定义

1.池化技术

2.内存池

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

4.malloc

定长内存池

什么是定长内存池?

定长内存池变量介绍

定长内存池申请一个对象大小空间实现

定长内存池释放一个对象大小空间

Threadcache

Threadcache设计框架

内存碎片问题

FreeList的结构

根据申请大小计算对齐数及桶号

一、计算对齐数

二、计算对应的哈希桶桶号

Threadcache类的设计

TLS无锁获取机制

一、申请内存对象

二、向中心缓存获取对象

三、释放内存对象

Centralache

Centralache的整体设计

 span的结构设计

 SpanList结构设计

 Centralache类的设计

获取一个新的Span

从中心缓存获取一定数量的对象给thread cache

把threadache中的对象还给span

 Pageache

Pageache的整体设计框架

Pageache类的设计

(难点)两个关系的转化与映射

页号与地址的对应

页号与span的对应关系

获取一个K页的span

映射函数设计

 pageache回收span

项目优化与测试

优化一  利用定长内存池代替new

优化二 释放对象时不传对象大小

优化三 利用基数树代替unordered_map优化

测试与总结


项目介绍

1.这个项目做的是什么?

          当前项目是实现一个高并发的内存池,他的原型是 google 的一个开源项目 tcmalloc tcmalloc 全称hread-Caching Malloc,即线程缓存的 malloc ,实现了高效的多线程内存管理,用于替代系统的内存分配相关的函数(malloc free )。
           我们这个项目是把 tcmalloc 最核心的框架简化后拿出来,模拟实现出一个自己的高并发内存池,目的就 是学习tcamlloc 的精华,这种方式有点类似我们之前学习 STL 容器的方式。但是相比 STL 容器部分,tcmalloc的代码量和复杂度上升了很多,大家要有心理准备。当前另一方面,难度的上升,我们的收获 和成长也是在这个过程中同步上升。
          另一方面 tcmalloc 是全球大厂 google 开源的,可以认为当时顶尖的 C++ 高手写出来的,他的知名度也是非常高的,不少公司都在用它,Go 语言直接用它做了自己内存分配器。

2.这个项目的要求的知识储备和难度?

         这个项目会用到 C/C++ 、数据结构(链表、哈希桶)、操作系统内存管理、单例模式、多线程、互斥锁

内存池的定义

1.池化技术

是在计算机技术中经常使用的一种设计模式,其内涵在于:将程序中需要经常使用的核心资源
先申请出来,放到一个池内,由程序自己管理,这样可以提高资源的使用效率,也可以保证本程
序占有的资源数量。 经常使用的池技术包括内存池、线程池和连接池等,其中尤以内存池和线程
池使用最多。

2.内存池

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

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

    内存池主要解决的当然是效率的问题,其次如果作为系统的内存分配器的角度,还需要解决一下内存碎片的问题。那么什么是内存碎片呢?
再需要补充说明的是内存碎片分为外碎片和内碎片,上面我们讲的外碎片问题。外部碎片是一些空闲的连续内存区域太小,这些内存空间不连续,以至于合计的内存足够,但是不能满足一些的内存分配申请需求。内部碎片是由于一些对齐的需求,导致分配出去的空间中一些内存无法被利用。内碎片问题,我们后面项目就会看到,那会再进行更准确的理解。

4.malloc

C/C++ 中我们要动态申请内存都是通过 malloc 去申请内存,但是我们要知道,实际我们不是直接去堆获取内存的,而malloc 就是一个内存池。 malloc() 相当于向操作系统 批发 了一块较大的内存空间,然后 零售 给程序用。当全部“ 售完 或程序有大量的内存需求时,再根据实际需求向操作系统 进货 malloc 的实现方式有很多种,一般不同编译器平台用的都是不同的。比如windows vs 系列用的微软自己写的一套,linux gcc用的 glibc 中的 ptmalloc 。下面有几篇关于这块的文章,大概可以去简单看看了解一下,关于ptmalloc,学完我们的项目以后,有兴趣大家可以去看看他的实现细节。

   

定长内存池

作为程序员 (C/C++) 我们知道申请内存使用的是 malloc malloc 其实就是一个通用的大众货,什么场景下都可以用,但是什么场景下都可以用就意味着什么场景下都不会有很高的性能 ,下面我们就先来设计一个定长内存池做个开胃菜,当然这个定长内存池在我们后面的高并发内存池中也是有价值的,所以学习他目的有两层,先熟悉一下简单内存池是如何控制的,第二他会作为我们后面内存池的一个基础组件。

什么是定长内存池?

我们设计这个内存池之前,我们先来设计一个定长的内存池,什么是定长内存池呢,它就像new 一个对象一样,每次new的时候对象的大小是固定的。也就是说申请内存的大小是已经创建出的类的对象固定大小,这个时候相当于我们为这个类创建一个内存池,每次申请的都是这个类的对象大小,为一个确定的值。

定长内存池变量介绍

template<class T>
class ObjectPool
{
 private:
	char* _memory = nullptr; // 指向大块内存的指针
	size_t _remainBytes = 0; // 大块内存在切分过程中剩余字节数
	void* _freeList = nullptr; // 还回来过程中链接的自由链表的头指针
};

因此,定长内存池当中包含三个成员变量:

  • _memory:指向大块内存的指针。
  • _remainBytes:大块内存切分过程中剩余字节数。
  • _freeList:还回来过程中链接的自由链表的头指针。

定长内存池申请一个对象大小空间实现

template<class T>
class ObjectPool
{
public:
	T* New()
	{
		T* obj = nullptr;

		// 优先把还回来内存块对象,再次重复利用
		if (_freeList)
		{
			void* next = *((void**)_freeList);
			obj = (T*)_freeList;
			_freeList = next;
		}
		else
		{
			// 剩余内存不够一个对象大小时,则重新开大块空间
			if (_remainBytes < sizeof(T))
			{
				_remainBytes = 128 * 1024;
				_memory = (char*)malloc(_remainBytes);
				if (_memory == nullptr)
				{
					throw std::bad_alloc();
				}
			}

			obj = (T*)_memory;
			size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
			_memory += objSize;
			_remainBytes -= objSize;
		}

		// 定位new,显示调用T的构造函数初始化
		new(obj)T;

		return obj;
	}

这里有一个很巧妙的构造,为了让释放后的内存块在freelist中链接起来,由于32位下,一个指针的大小是4字节,所以我们可以取这个对象的头四个字节的内容填充下一个回收对象的首地址,这样就实现了把他们链接起来。

还有一处很好的设计,就是我们在32位下指针是4个字节,但是在64位下,指针就是8个字节,我们应该如何来做呢?

*(void**)在这里就是一个很好的设计,当在64位下的时候,void*是指针类型8字节,void**也是指针类型,8字节,我对void**解引用,得到的是void*8字节,这个时候,我们就可以在64位下拿到前8个字节,而32位下,就void**照样是4个字节。这样就使得了void**在32位和64位下都适用了。

总结申请步骤:先看回收链表中有没有剩余,如果有的话,将剩余链表中头删一个对象,然后给obj。如果没有了,就看下现在的剩余内存池够不够创建一个对象的,如果不够的话,找系统申请一个128K的空间,然后在新申请的内存空间中切一个obj给他,当然还有一种情况,就是确实有剩余,但是对象本身不够一个指针大小(32位下4字节),这个时候我们就可以直接给他一个指针大小就可以了。

定长内存池释放一个对象大小空间

void Delete(T* obj)
	{
		// 显示调用析构函数清理对象
		obj->~T();

		// 头插
		*(void**)obj = _freeList;
		_freeList = obj;
	}

 就是一个简单的头插即可,但是这个地方要注意的是我们在申请和释放的时候都需要显示的调用我们的构造函数和析构函数,因为在这里创建对象和删除对象的操作都是我们自己写的接口,一旦我们自己写了,系统就不会进行自动调用,我们就必须显示的调用构造函数和析构函数来进行该类的初始化和清理工作,因为构造函数和析构函数的作用并不是仅仅开辟空间和销毁空间,他们也有可能在其中做了很多其他的操作,所以我们必须进行显示调用。​​​​​​

高并发内存池整体框架设计
现代很多的开发环境都是多核多线程,在申请内存的场景下,必然存在激烈的锁竞争问题。 malloc 本身其实已经很优秀,那么我们项目的原型tcmalloc 就是在多线程高并发的场景下更胜一筹,所以这次我们
实现的 内存池需要考虑以下几方面的问题
1. 性能问题。
2. 多线程环境下,锁竞争问题。
3. 内存碎片问题。
concurrent memory pool 主要由以下 3 个部分构成:
1. thread cache :线程缓存是每个线程独有的,用于小于 256KB 的内存的分配, 线程从这里申请内 存不需要加锁,每个线程独享一个 cache ,这也就是这个并发线程池高效的地方
  2. central cache :中心缓存是所有线程所共享, thread cache 按需从 central cache 中获取
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值