C++高并发内存池实战:初步了解

本文深入探讨了C++高并发内存池的实现原理与技术细节。首先介绍了高并发系统的特点与内存池的核心概念,包括预分配内存、固定大小内存块等关键特性。重点分析了定长内存池的实现方法,详细展示了跨平台的系统调用封装(Malloc)、内存分配(New)与释放(Delete)接口的实现代码。文章最后勾勒出高并发内存池的三层架构:线程级缓存ThreadCache减少锁竞争,中心缓存CentralCache作为中介层,以及直接与操作系统交互的PageCache。该架构通过分层设计有效提升了多线程环境下的内存分配效率,为后续深

作者
WildPointer
发布
2024.05.12
专栏
C++高并发内存池实战
阅读
约 8 分钟 / 1,231 次原文浏览
C++高并发内存池实战:初步了解

前言

针对前篇文章《源码剖析:STL中空间配置器(allocator)》中所描述的二级配置器小节中关于内存池的讲述,本人将专门对C++高并发内存池的理论以及实现细节进行针对性讲解。本篇文章将简单的对C++高并发内存池的理论以及结构分层进行描述,后续文章将通过分层的方式,对C++高并发内存池的理论以及细节进行讲解,还会配上实现源码方便读者理解。


什么是高并发?

高并发是指系统能够同时处理大量并行请求或操作的能力。在项目例程中往往会存在一个进程含有多个线程的情况,当多个线程同时向内存池申请空间分配时,高并发的能力变得尤为重要,一个稳定的,快速的处理并发的操作,使高并发内存池的关键


什么是内存池?

内存池指的是一种内存管理技术,特别是在需要频繁进行内存申请和释放的场景中,为了优化动态内存分配和释放的性能所实现。其核心思想是向系统预先分配一块内存,然后在这块内存中以更高效的方式管理和分配内存块

以下是内存池的几个关键特点:

                1.预分配内存:在内存池初始化时,会预先从操作系统申请一块内存,用于内存分配

                2.固定大小的内存块:通常包含多个固定大小的内存块,这些内存块可以被重复使用,减少了内存分配和释放的开销

                3.减少内存碎片:由于内存池中的内存块大小固定,可以减少内存碎片的产生,提高内存使用效率

                4.提高性能:减少系统调用和内存复制,显著提高内存分配和释放的速度

                5.简化内存管理:不需要直接使用malloc()函数和free()函数等管理内存,只需与内存池交互

                6.多线程支持:支持多线程环境(高并发),能够在没有锁或使用锁的情况下安全地进行内存分配和释放

                7.灵活性:支持不同大小的内存块,以适应不同的内存分配需求


什么是池化技术?

池化技术指的是进程先向系统申请过量的资源,然后自行进行管理,以备不时之需。由于向系统申请空间等资源会产生上下文切换操作,导致系统开销较大,所以提前申请资源,当需要资源时则可以直接从"池"中获取,不需要时就将该资源重新放回"池"中即可,从而达到提高程序的运行效率的目的


什么是定长内存池?

定长内存池指的是针对固定大小内存块的申请和释放的内存池,所以在实现定长内存池时不需要考虑内存碎片等问题。接下来将对如何简单实现一个定长内存池进行讲解,并使用代码对其实现:

1.导入相关库:

1.<unistd.h>头文件:包含一组通用的Unix系统调用,这些调用操作与POSIX标准兼容

2.<windows.h>头文件:包含编写 Windows 应用程序时与操作系统进行交互的接口

3.<sys/mman.h>头文件:包含内存映射的函数和常量,常用于实现文件I/O操作

cpp
#pragma once    //防止重复包含该文件#include <iostream>using std::cout;using std::endl;#ifdef WIN32    //在Windows系统上运行    #include <windows.h>#else           //Linux或Unix系统上运行    #include <sys/mman.h>    #include <unistd.h>#endif

        2.实现定长内存池成员:

cpp
char* _memory = nullptr;    //指向向系统申请的内存地址size_t _remainBytes = 0;    //记录内存池剩余字节数void* _freeList = nullptr;  //链表,用于管理内存块

图1.定长内存池成员设计讲解图

        3.实现内存分配接口:

               1.MEM_COMMIT:用于将内存区域标记为已提交状态。意味着操作系统将分配物理存储器(如RAM)给指定的内存区域,使其可被进程访问和修改

                2.MEM_RESERVE:用于保留或预订内存地址空间,但并不立即分配物理存储。与MEM_COMMIT一起使用时,可以先预订地址空间,再逐步提交为可访问的内存

3.PAGE_READWRITE:用于定义内存的访问权限,表示内存页面可以被读取和写入

4.PROT_READ:用于在Unix或Linux系统中指定映射的内存区域具有读权限

5.PROT_WRITE:用于在Unix或Linux系统中指定映射的内存区域具有写权限

6.MAP_PRIVATE:用于创建一个私有的内存映射。对私有映射的修改不会反映到文件上,也不会被其他映射该文件的进程看到

7.MAP_ANONYMOUS:用于创建一个匿名映射,意味着映射不与任何文件关联

8.MAP_FAILEDD:用于标记mmap()函数调用失败

cpp
inline static void* Malloc(size_t RAM_Size){    void* ptr;#ifdef _WIN32    ptr = VirtualAlloc(0, RAM_Size << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);    //Windows下使用VirtualAlloc()进行内存分配#else    ptr = mmap(NULL, RAM_Size << 13, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);    //Linux下使用mmap()进行内存分配#endif    if (ptr == MAP_FAILED){ //检查内存是否分配失败        throw std::bad_alloc();    //抛出bad_alloc()异常    }    return ptr;}

        4.定长内存池对象应实现的函数:

                1.New()函数:

cpp
template<class T>T* New()    //实现定长内存池分配空间函数{    T* obj = nullptr;    //定义obj指针指向分配的内存地址    if (_freeList != nullptr){    //判断链表中是否存在空闲内存        obj = (T*)_freeList;    //优先使用分配过的内存        _freeList = *(void**)_freeList;    //强转为void**后解引用为void*        //用于判断下一个空闲指针的位置,使链表指针指向下一个空闲地址    }    else    {        size_t objSize = sizeof(T) > sizeof(void*) ? sizeof(T) : sizeof(void*);        //确保至少能存储一个指针大小        if (_remainBytes < objSize)    //申请的内存空间不足        {            _remainBytes = 128 * 1024;            _memory = (char*)Malloc(_remainBytes >> 13);            //将空闲地址中的13个内存分配给_memory        }        obj = (T*)_memory;        _memory += objSize;        _remainBytes -= objSize;    }    new(obj)T;    //原地构造new(),调用对象构造函数在指定的地址上初始化    return obj;}

                2.Delete()函数:

cpp
template<class T>void Delete(T* obj){	obj->~T();    //调用析构函数清理对象	*(void**)obj = _freeList;    //使用头插法,将归还的内存插入链表	_freeList = obj;}

5.整体实现:

cpp
#pragma once#include <iostream>using std::cout;using std::endl;#ifdef _WIN32    #include<windows.h>#else    #include<sys/mman.h>    #include<unistd.h>#endifinline static void* Malloc(size_t RAM_Size){    void* ptr;#ifdef _WIN32	ptr = VirtualAlloc(0, RAM_Size << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);#else	ptr = mmap(NULL, RAM_Size << 13, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);#endif	if (ptr == nullptr){		throw std::bad_alloc();    }	return ptr;}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 >> 13);				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(obj)T;		return obj;	}	void Delete(T* obj){		obj->~T();		*(void**)obj = _freeList;		_freeList = obj;	}private:	char* _memory = nullptr;	size_t _remainBytes = 0; 	void* _freeList = nullptr;};

高并发内存池的整体框架

此次只针对高并发内存池的整体架构进行简单描述,了解ThreadCache,CentralCache和PageCache三个分层的简单概念:

1.ThreadCache:是与特定线程直接关联的缓存,为每个线程提供了一个私有的内存分配空间。每个线程可以快速地从自己的ThreadCache中分配和释放内存,而无需进入全局锁或与其他线程争用,从而减少了锁竞争和上下文切换,提高了内存分配的效率

                2.CentralCache:作为一个全局或中心缓存存在,是多个ThreadCache之间的中介。当某个线程的ThreadCache耗尽时,它可以从CentralCache中获取内存块(通常是较大的内存块),然后再将这些内存块细分为适合ThreadCache使用的大小

                3.PageCache:是直接与操作系统的内存页面交互的缓存级别。负责管理大的内存区域(页面),并将这些页面分配给CentralCache或直接映射到ThreadCache

图2.C++高并发内存池框架图

26次原文点赞;这里的喜欢仅保存在本机
WildPointer

专注系统编程、工程实践与底层技术,记录 C++、Qt、OpenCV 与 VTK 的学习和实践。