【项目】高并发内存池实现(化简版tcmalloc)

目录

前言

项目要求的知识储备和难度

一、是什么是内存池

1.池化技术

2.内存池 

 二、为什么要使用内存池

1.主要就是效率问题

2.内存碎片问题 

3.malloc 

三、设计一个定长的内存池 

1.定长内存池设计

2.详细步骤

 3.代码

四、高并发内存池整体框架设计

五、高并发内存池--thread cache 

 1.thread cache设计 

2.详细步骤

 六、高并发内存池--central cache

1.central cache设计

2.详细步骤

七、高并发内存池--page cache

1.page cache设计

2.详细设计

八、申请流程串联调试

九、回收多余的内存块

十、释放流程串联调试 

十一、优化

1. 大于256KB的大块内存申请问题

2. 使用定长内存池配合脱离使用new。

3.释放对象时优化为不传对象大小

十二、性能测试

1.多线程环境下对比malloc测试 

十三、扩展


前言

        因为偶然的机会,我通过同学那里知道这个google有一个开源项目tcmalloc,他讲的头头是道,而我也对其非常感兴趣。

        这个tcmalloc呢,全称Thread-Caching Malloc,通过名字就能看出跟线程相关,也确实如此,它就叫线程缓存的malloc,其中实现了高效的多线程内存管理,用于替代系统的内存分配相关的函数(malloc、free)。由于new和delete底层也是通过malloc和free实现的,所以这个项目很有意义。

        而这个项目是由google的C/C++高手写出来的,高手出手,不同凡响。google是一个老牌的C/C++大厂,这个公司里写出来的项目对我们的技术方面,肯定有不小提升,我们也可以通过这个项目,与当初写tcmalloc的程序员来一次跨越时间的会面,了解真正的大佬是如何进行程序开发的。

        我们这个项目是把tcmalloc最核心的框架简化了之后再去实现,模拟实现出一个属于自己的高并发内存池,简单的来说目的就是学习tcmalloc的精华。

       tcmalloc源代码

项目要求的知识储备和难度

        这个项目需要用到C/C++、数据结构(链表、哈希桶)、操作系统的内存管理、单例模式、多线程、互斥锁等这方面的知识。

        难度的话,是有的,并且还不低。不过这对于正在奋斗向上,迎难而上的C/C++程序员来说,不过是些许风霜罢了。

        因为我想要学习这个项目,阅读了很多别人写的博客,所以我了解博客写的不详细对看的人来说是很难受的,我也想希望别人通过阅读这篇博客,从而对我思想和知识方面提出些疑问,让我们继续共同进步。区区拙作,望能斧正。

一、是什么是内存池

1.池化技术

        举个例子:以前我过年回老家的村子上,去我二舅家,那时候还没有家家户户通自来水,我经常口渴,所以缠着长辈们想喝水,他们就带我去他们家里的厨房,角落里有一个大大的陶制水缸,他们就从那里舀水给我喝(当然,提醒大家,喝水还是建议喝烧开的水为好),喝下去,我就不渴了。然而我这时突然想,这水是怎么来的?

        带着疑问,来到了第二天,我看到了我二舅推着一个很大的推车,推车上有一个大大的蓝色的铁皮罐子,还需要后面跟着我的几位哥哥齐步推着,当他们把水倒入到家里的几个水缸时,我就在想,弄一次水好辛苦啊!真该一次性弄多一些,要不然隔三岔五跑一会,多耽误人的时间。

        所以,我们一次弄够足够多的水在家里,以备不时之需,当然是把水弄得多多的。这样效率才是足够多。如果我们想喝水,还得此次跑去水站去接水,我们把接来的水使用一个容器来装起来,也就是大大的陶制水缸,这样我们用水时,只需从水缸里舀水即可。

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

2.内存池 

        内存池(Memory Pool) 是一种动态内存分配与管理技术。 通常情况下,程序员习惯直接使用 new、delete、malloc、free 等API申请分配和释放内存,这样导致的后果是:当程序长时间运行时,由于所申请内存块的大小不定,频繁使用时会造成大量的内存碎片从而降低程序和操作系统的性能。内存池则是在真正使用内存之前,先申请分配一大块内存(内存池)留作备用,当程序员申请内存时,从池中取出一块动态分配,当程序员释放内存时,将释放的内存再放入池内,再次申请池可以再取出来使用,并尽量与周边的空闲内存块合并。若内存池不够时,则自动扩大内存池,从操作系统中申请更大的内存池。

 二、为什么要使用内存池

1.主要就是效率问题

再举个例子,我们现在还是学生,生活费还得爸爸妈妈要,而如果我们买一份中午饭,12块钱,拿起微信,问妈妈要,餐厅窗口工作人员得等着你,后面排队的学生也要等着你,你也要等着妈妈来给你钱,都等在那里,那就是太糟糕的情况了!只能幸亏电脑的二进制没有情绪,不然走不出这个程序,他们就会杀掉你,开个玩笑。所以我们每次问妈妈要足够的钱到你的微信账户上,再每次付钱的时候,用你自己微信账户上的钱付钱,没钱了再要一笔钱,这样效率就会大大提升了!计算机同样也是如此,程序就像是上学的童鞋,操作系统就像父母,频繁申请内存的场景下,每次需要内存,都像系统申请效率必然有影响。

2.内存碎片问题 

我们每次申请内存是在内存的是什么地方呢?是在一个叫堆的地方,如下图linux下进程地址空间

而当我释放时,因为申请内存空间的释放是自由的,就会导致下图情况

而一部分释放了,一部分还在保持着,导致内存碎片化,再想要申请大内存空间就可能申请不下来了。

补充:

内存碎片有两种碎片

1.外碎片,就是上面这种情况

2.内碎片,因为各种数据结构内存对齐的原因,导致一些内存用不上,空着。

3.malloc 

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

三、设计一个定长的内存池 

        作为C/C++程序员我们知道申请内存使用的是malloc,malloc在任何场景下都很通用,但是一个问题是在什么场景下都可以使用就意味着什么场景下都可能不会有很高的性能。下面我们先通过设计一个定长内存池,来简答熟悉一下简单内存池是如何控制的,第二它会作为我们后面内存池的一个基础组件。

进行改进 

1.定长内存池设计

如果我们申请一大块空间,因为内存的释放是不存在分期付款的,需要一次性释放。

如果一次性归还,那这个内存池就太low了,我们只能申请一次。所以我们申请固定大小的内存,这个申请的内存容量固定大小,肯定是合适的,不会太大,那么我们肯定不止需要一份,我们需要很多份。

所以我们需要一个_freeList链表来对这些空间进行管理即可。

固定大小内存申请释放需求特点:

  1. 性能达到极致
  2. 不考虑内存碎片问题

 我们先建立一个头文件:ObjectPool.h

注:以下代码有一个问题,最后统一改,同学们可以先找找是哪的错,提示一下,是关于类型强转以达到固定长度空间的问题。

2.详细步骤

1.先申请一个大的内存_memory,每次我们需要使用内存时,只需要,切一个固定长度的内存来使用,不断使用,不断切,直到大块内存_memory使用完成后,让_memory再去系统申请即可。

2.而当我们切的这些块,我们归还回来后,我们该怎么管理呢

我们可以使用链式结构,把这些内存管理起来

我们可以把这些申请的块,看成一个个结点,每个结点存下一个结点的地址,再用一个_freelist指向最开始的结点即可。

3.假如有一个T类型的对象,要申请一块空间,我们让obj指向_memory指向的同一个地方,让_memory向后移动sizeof(T)大小的距离,这就可以分配给obj一个定长大小sizeof(T)的内存了。

大家来看这份初始的New内存的代码有什么问题吗?

如果obj将最后一份空间也申请走了,_memory+=sizeof(T),这时已经没有像系统申请的空间了,但是_memory不为空。

所以我们这样做

但是这样依旧不好,因为,T类型有很多种,有int,double,float,还有自定义类型等,当我们的obj申请sizeof(T)的内存时,到了最后,剩余的内存小于sizeof(T),这样依旧会出现问题。所以最后应该这么改

4.当我想要释放某个内存时,我可以将内存回收,通过结点头部的4个字节或8个字节(根据自己的系统是32位还是64位为准),指向下一个结点,让自由链表的最后一个结点头部指向空。

下面这断代码,当自由链表为空时,通过使指针指向的内存块前4个字节或者前8个字节内容为空,意为下一个结点为空。

但是当64为系统下时,int*强转后解引用,依旧是4个字节,而64为系统下指针是8个字节。

所以这里通过将obj强转成void**再解引用,这里不管什么void**还是int**,都可以。

int*解引用是个int,就取一个int的大小的空间。

void**解引用是个void*,就一个指针大小的空间,指针大小,32位是4,64位是8.这样就符合要求了。

那如果我们在一个已经存在结点的自由链表中怎么插入呢?难道要尾插吗,大可不必,因为结点就是一个内存块,毫无意义,直接头插即可

我们释放的内存也可以回收使用啊,所以先判断自由链表中有无结点,如果有,因为是定长内存池,所以每一个分配的内存大小一致,所以就可以给用户继续使用。

我们定义一个next类型的指针,指向自由链表中结点的内容,也就是第二个结点,可以就可以实现头删操作,从而顺利的将内存交到用户手中。

避免因为int或char字节数过小,而导致freelist无法使用,所以统一定成指针大小的空间。
            size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);

好了,这个New就没什么问题了。

前面的问题在于

 3.代码

#pragma once

#include <iostream>


//使用using namespace std;会导致污染,在项目中,把常用的展开即可
using std::cout;
using std::endl;

#ifdef _WIN32
	#include <windows.h>
#else
	//Linux
#endif


方案一:定长N大小的内存池
//template<size_t N>  
//class ObjectPool
//{
//};

//直接去堆上按页申请空间,脱离malloc
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
	void* ptr = VirtualAlloc(0, kpage << 13 /*8KB*/, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
	// linux下brk mmap等
#endif
	if (ptr == nullptr)
		throw std::bad_alloc();
	return ptr;
}


//方案二:获取的对象每次都是一个T对象,T的大小是固定的,所以内存池申请的内存也是固定的
template<class T>
class ObjectPool
{
public:
	T* New()
	{
		T* obj = nullptr;
		//换回来的内存可以重复使用
		//优先把换回来的内存再次重复利用
		if (_freeList != nullptr)
		{
			void* next = *(void**)_freeList;
			obj = (T*)_freeList;
			_freeList = next;
		}
		else
		{
			//T类型有很多种,有int,double,float,还有自定义类型等,当我们的obj申请sizeof(T)的内存时,到了最后,剩余的内存小于sizeof(T),这样依旧会出现问题。
			//当剩余内存不够一个对象大小时,那么重新开空间。
			if (_remainBytes < sizeof(T))
			{
				_remainBytes = 128 * 1024;
				//_memory = (char*)malloc(_remainBytes); //申请一个固定大小256KB的空间
				_memory = (char*)SystemAlloc(_remainBytes >> 13);
				if (_memory == nullptr)
				{
					throw std::bad_alloc();
				}
			}
			obj = (T*)_memory;
			//避免因为i
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值