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

目录

系列文章

项目介绍

定长内存池 

内碎片和外碎片

malloc的工作原理

实现过程

定位new 

VirtualAlloc函数

封装VirtualAlloc

最终代码


系列文章

  1. 高并发内存池(二):​ThreadCache、通用函数、自由链表的初步实现
  2. 高并发内存池(三):CentralCache与PageCache的实现
  3. 高并发内存池(四):阶段性代码展示 与 申请内存过程的调试
  4. 高并发内存池(五):内存回收机制、阶段性代码展示和释放内存过程的调试
  5. 高并发内存池(六):补充内容

项目介绍

项目原型:goole的开源项目tcmalloc(Thread-Caching Malloc)

项目目标:实现高效的多线程内存管理,替代系统提供的内存分配相关的函数(malloc等)

涉及技术栈:C/C++、数据结构(链表、哈希桶)、操作系统的内存管理、单例模式、多线程、互斥锁、慢调节算法、池化技术

池化技术: 程序提前向系统申请过量的资源,然后自行管理,从而减少每次申请资源时的开销,提高程序运行效率(线程池的主要思想是:先启动若干数量的线程,让它们处于睡眠状态,当接收到客户端的请求时,唤醒池中的某个睡眠的线程,让它来处理客户端的请求,当处理完请求后,该线程继续进入睡眠状态)

定长内存池 

基本概念:预先从OS中申请一块足够大的内存,后续的内存申请和内存释放都是基于这块足够大的内存,而不是向堆申请新的内存,提高了内存申请和释放的效率,直到程序全部结束这块大内存才会释放

内碎片和外碎片

基本概念:内碎片指系统分配的但没用完的内存,外碎片指系统还可分配的内存

问题分析:如下图所示,在申请一块300Byte的空间时,由于两个大块外碎片的空间并不相连所以申请失败,而如果申请了20Byte的空间但系统分配了1000Byte,内碎片的浪费率就为98%太大了

结论:外碎片过多会导致总内存足够,但内存空间可能不连续,不能满足一些较大的内存申请;内部碎片过多会导致内存浪费

malloc的工作原理

基本概念:malloc本质上就是一个大的内存池,调用malloc就相当于向操作系统“批发”一大批内存空间,然后“零售”给程序使用,当全部“售完”或程序有更大的内存需求时,再根据需求向操作系统“进货”,各个平台的malloc的实现方式都是不同的(windows下是对系统调用接口VirtualAlloc的封装,linux下是对系统调用接口brk的封装)

实现过程

1、_memory指向从堆申请的大块内存(这里规定申请128Kb),然后每次为T类型对象分配所需内存(基于它一个指向该块内存的指针),向后移动_memory

class ObjectPool
{
public:
    //申请内存
    T* New()
    {
        T* obj = nullptr;//接收分配的内存的指针
        if(_memory == nullptr)//第一次进来先申请一个大块内存
        {
            _memory = (char*)malloc(128 * 1024);
            if(_memory == nullptr)
            {
                throw std::bad_alloc();//申请失败就抛异常
            }
        }
        obj = (T*)_memory;
        _memory += sizeof(T);
        return obj;
    }
private:
    char *_memory = nullptr;//指向申请的大块内存的指针
}

2、128Kb的内存被用完时,下次申请进行移动_memory时就会越界,所以当剩余内存不满足一个T类型对象的大小时(_remainBytes < sizeof(T)),需要重新申请大块内存

class ObjectPool
{
public:
    //申请内存
    T* New()
    {
        T* obj = nullptr;
       
     	//剩余内存不够一个T类型对象的大小时,重新申请大块内存
	    if (_remainBytes < sizeof(T))
	    {    
            _remainBytes = 128 * 1024;//保证每次重新申请的大块内存均为128Kb
            _memory = (char*)malloc(_remainBytes);
            if(_memory == nullptr)
            {
                throw std::bad_alloc();//申请失败就抛异常
            }
	    }
   
        obj = (T*)_memory;
        _memory += sizeof(T);
        _remainBytes -= sizeo(T);//每次分配后重新计算大块内存可用的字节数

        return obj;
    }
private:
    char *_memory = nullptr;//指向申请的大块内存的指针
    size_t _remainBytes = 0;//大块内存中剩余可分配字节数
}

3、也不能逮着一个内存块狠用,也要将程序使用完归还的内存块利用起来,而利用的前提是得有地方存放,这个地方就是自由链表,其中的每个结点的前4/8个字节用于存放下一个结点的地址,新结点以头插的方式放进自由链表

class ObjectPool
{
public:
    //申请内存
    T* New()
    {
        ...
    }

    //回收内存
    void Delete(T* obj)//obj指向要回收的对象的指针
    {        
        //头插
        *(void**)obj = _freelist;
        _freelist = obj;
    }
private:
    char *_memory = nullptr;//指向申请的大块内存的指针
    size_t _remainBytes = 0;//大块内存中剩余可分配字节数
    void* _freelist = nullptr;//指向存放归还回来内存结点的自由链表
}

4、因为32位下一个指针为4字节和64位环境下为8字节,而int始终为4字节,在32位机器下使用*(int*)obj 的方式使obj指向的内存结点的前4字节存放下个结点的地址是没问题的,但是如果是64位环境,解引用后只能获取前4个字节,获取的地址是实际的一半,所以我们采用解引用二级指针的方式,这样就不需要我担心当前程序的运行环境了(解引用二级指针得到的都是一级指针,32位下一级指针表示4字节就让前4字节为空,64位下一级指针表示8字节就让前8字节为空)

5、有了自由链表,我们就可以将不用一直逮着大块内存池薅了,如果有内存申请且当前的自由链表中有结点,就将该结点使用头删释放出去 

class ObjectPool
{
public:
    //申请内存
     T* New()
    {
        T* obj = nullptr;
        
        if(_freelist != nullptr)
        {    
            //头删
            void* next = *((void**)_freelist);//next指向自由链表的第二个结点
            obj = _freelist;
            _freelist = next;
            return obj;//返回指向从自由链表中分配的结点的指针
        }
        else
        {
            ....
        }
    }

    //回收内存
    void Delete(T* obj)//obj指向要回收的对象的指针
    {        
        ...
    }
private:
    char *_memory = nullptr;//指向申请的大块内存的指针
    size_t _remainBytes = 0;//大块内存中剩余可分配字节数
    void* _freelist = nullptr;//指向存放归还回来内存结点的自由链表
}

6、若T类型的对象占用的字节数小于当前环境中一个指针的大小,就无法链接其它结点,因此我们要保证即使T类型对象本身所需内存过小也能记录下一个结点的位置

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

定位new 

作用:在已分配好的内存空间中调用某对象的构造函数实例化一个该对象,一般配合内存池使用

格式:

格式一:new (place_address) type 
格式二:new (palce_address) type (initializer_list)
  • place_address:指向待构造对象的指针
  • type:待构造对象的类型
  • initializer_list:待构造对象的初始化列表

注意事项:

  1. 需要手动管理内存:使用定位new时,程序员必须先分配内存,并确保这块内存足够大,能够容纳将要构造的对象。此外,还需要负责这块内存的释放。

  2. 不进行内存分配:定位new只调用对象的构造函数,不会像new运算符那样分配内存。因此,如果提供的内存不足,会引发未定义行为。

  3. 不能使用默认构造函数:如果没有为定位new提供的内存地址提供一个合适的构造函数,编译器将无法调用默认构造函数,除非该构造函数已经在类定义中显式声明

  4. 需要显式调用析构函数(重要):由于定位new不包括分配和释放内存的代码,因此必须显式地调用对象的析构函数来销毁对象,以避免内存泄漏。

  5. 处理数组:如果使用定位new来创建一个对象数组,那么构造每个对象时都需要分别调用定位new,同时在数组被销毁时,需要为每个对象分别调用析构函数

VirtualAlloc函数

基本概念:为了使得定长内存池不使用malloc,我们可以使用Windows和Linux均有提供的直接向系统申请以页为单位的大块内存的接口,Windows是VirtualAlloc,Linux是brk()和mmap()  

参考链接:VirtualAlloc 函数 (memoryapi.h) - Win32 apps | Microsoft Learn 

函数原型: 

LPVOID VirtualAlloc(
  [in, optional] LPVOID lpAddress,
  [in]           SIZE_T dwSize,
  [in]           DWORD  flAllocationType,
  [in]           DWORD  flProtect
);
  • lpAddress可选参数,指定希望分配的虚拟内存的起始地址。若传入 NULL,系统自动分配
  • dwSize指定要分配的内存区域大小,单位为字节
  • flAllocationType标志位(可多个),我们这里使用了MEM_COMMIT | MEM_RESERVE这两个标志位结合,这表示VirtualAlloc函数会尝试为调用进程分配一块指定大小的内存区域,并立即为这块内存分配物理存储器。这样做的好处是确保了内存区域既不会被其他分配占用,也可以立即被访问
  • flProtect指定分配的内存页面的保护属性,我们这里选择PAGE_READWRITE表示可读写访问

封装VirtualAlloc

基本概念:通过对VirtualAlloc函数进行封装,我们就可以写出一个避开malloc直接向操作系统申请内存的自定义函数,就可以将后续使用malloc的场景直接替换为SystemAlloc函数

//这里使用Windows开发环境
inline static void* SystemAlloc(size_t kpage)//kpage表示页数
{
	#ifdef _WIN32
		void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
	#endif
		if (ptr == nullptr)
			throw std::bad_alloc();//抛异常
	return ptr;
}
  • static inline的解释: SystemAlloc函数被建议内联展开,并且它是一个文件内部的静态函数,它的作用域被限定在了定义它的文件内
  • VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE):在进程的虚拟地址空间中申请一块大小为 kpage * 8192 字节的区域,这块内存既被预留也被提交,并且具有可读写的属性

最终代码

template<class T>//模板参数T
class ObjectPool
{
public:
    //封装VirtualAlloc,直接向堆申请以页为单位的内存
	inline static void* SystemAlloc(size_t kpage)//kpage表示页数
	{
		#ifdef _WIN32
			void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
		#endif
			if (ptr == nullptr)
				throw std::bad_alloc();
		return ptr;
	}

    //为T类型的对象构造一大块内存空间
    T* New()
    {
	    T* obj = nullptr;

	    if (_freelist != nullptr)
    	{
	    	//头删
	    	void* next = *((void**)_freelist);
	    	obj = _freelist;
	    	_freelist = next;
	    	return obj;
	    }
	    else//自由链表没东西才会去用大块内存
	    {
		    //剩余内存不够一个T对象大小时,重新开大块空间
		    if (_remainBytes < sizeof(T))
		    {
			    _remainBytes = 128 * 1024;
		    	_memory = (char*)SystemAlloc(_remainBytes >> 13);//SystemAlloc替换了原来这里的malloc
		    	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;
    }

    //回收内存
    void Delete(T* obj)//obj指向要回收的对象的指针
    {
        //显示调用析构函数清理对象
	    obj->~T();
        
        //头插
        *(void**)obj = _freelist;
        _freelist = obj;
    }

private:
    char *_memory = nullptr;//指向申请的大块内存的指针
    size_t _remainBytes = 0;//大块内存中剩余可分配字节数
    void* _freelist = nullptr;//指向存放归还回来内存结点的自由链表
};

~over~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值