提示:高并发内存池完整项目代码,在主页专栏项目中
文章目录
先设计一个定长的内存池
作为程序员(C/C++)我们知道申请内存使⽤的是malloc,malloc其实就是⼀个通⽤的⼤众货,什么场景 下都可以⽤,但是什么场景下都可以⽤就意味着什么场景下都不会有很⾼的性能,下⾯我们就先来设 计⼀个定⻓内存池做个开胃菜,当然这个定⻓内存池在我们后⾯的⾼并发内存池中也是有价值的,所 以学习他⽬的有两层,先熟悉⼀下简单内存池是如何控制的,第⼆他会作为我们后⾯内存池的⼀个基础组件。
#include <iostream>
#include <vector>
#include <time.h>
using std::cout;
using std::endl;
#ifdef _WIN32
#include<windows.h>
#else
//
#endif
// 定长内存池
//template<size_t N>
//class ObjectPool
//{};
// 直接去堆上按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
void* ptr = VirtualAlloc(0, kpage<<13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
// linux下brk mmap等
#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);
_memory = (char*)SystemAlloc(_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,显示调用T的构造函数初始化
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; // 还回来过程中链接的自由链表的头指针
};
一、为什么需要定长内存池?
在C++开发中,频繁的内存分配和释放是性能瓶颈的主要来源之一。让我们先看一个现实中的比喻:
🏢 传统内存分配的痛点
想象每次需要办公桌时都现买:
⏰ 时间开销大:每次都要去家具市场
💰 成本高昂:中间商赚差价(内存碎片)
🎯 效率低下:无法批量优化
🏭 内存池的解决方案
像大型办公室统一采购:
🚀 批量获取:一次性申请大块内存
⚡ 快速分配:直接从池中分配,无需系统调用
🔄 重复利用:释放的内存放回池中复用
📦 减少碎片:固定大小分配,无外部碎片
二、定长内存池核心设计思想
1. 整体架构
三大核心组件:
🗃️ 大块内存:从系统申请的内存块
🔗 空闲链表:管理已释放可重用的内存块
📊 分配策略:决定如何分配新内存
2. 类定义解析
template<class T>
class ObjectPool
{
private:
char* _memory = nullptr; // 指向大块内存的指针
size_t _remainBytes = 0; // 剩余可用字节数
void* _freeList = nullptr; // 空闲链表头指针
};
三、关键技术实现深度解析
1. 内存申请策略
// 直接去堆上按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
// Linux下使用brk或mmap等系统调用
#endif
if (ptr == nullptr)
throw std::bad_alloc();
return ptr;
}
设计要点:
🖥️ 跨平台支持:Windows使用
VirtualAlloc
,Linux使用mmap
📏 按页分配:以页面为单位(通常4KB),减少系统调用次数
🚨 异常安全:分配失败抛出
bad_alloc
异常
2. 内存分配(New方法)
T* New()
{
T* obj = nullptr;
// 优先复用空闲链表中的内存块
if (_freeList)
{
void* next = *((void**)_freeList); // 获取下一个空闲块
obj = (T*)_freeList; // 当前块作为分配对象
_freeList = next; // 更新空闲链表头
}
else
{
// 剩余内存不足时申请新的大块内存
if (_remainBytes < sizeof(T))
{
_remainBytes = 128 * 1024; // 申请128KB
_memory = (char*)SystemAlloc(_remainBytes >> 13); // 计算页数
if (_memory == nullptr)
{
throw std::bad_alloc();
}
}
// 从大块内存中切分
size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
obj = (T*)_memory;
_memory += objSize;
_remainBytes -= objSize;
}
// 定位new调用构造函数
new(obj)T;
return obj;
}
关键技术点:
空闲链表优先:先尝试从空闲链表获取已释放的内存
内存对齐:确保每个对象至少
sizeof(void*)
大小,便于链表操作批量申请:一次性申请128KB内存,减少系统调用
定位new:在指定内存地址调用构造函数
3. 内存释放(Delete方法)
void Delete(T* obj)
{
// 调用析构函数清理对象
obj->~T();
// 头插法将内存块加入空闲链表
*(void**)obj = _freeList;
_freeList = obj;
}
设计精髓:
🧹 资源清理:显式调用析构函数
🔗 链表管理:使用头插法将释放的内存加入空闲链表
⚡ 高效复用:释放的内存立即可用于下次分配
四、空闲链表技术的巧妙运用
1. 链表存储原理
关键技巧: 利用内存块本身存储链表指针
// 释放时:将当前内存块的前4/8字节存储下一个节点的地址
*(void**)obj = _freeList; // 将_freeList值存入obj指向的内存
_freeList = obj; // 更新链表头
// 分配时:从链表头取出节点,并更新头指针
void* next = *((void**)_freeList); // 读取下一个节点地址
obj = (T*)_freeList; // 当前节点作为分配对象
_freeList = next; // 更新链表头
2. 内存对齐的重要性
size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
为什么需要对齐?
📏 最小大小:确保每个内存块至少能存储一个指针(4或8字节)
🧩 地址对齐:保证指针操作的正确性
⚡ 访问效率:对齐的内存访问速度更快
五、性能优势分析
与传统malloc对比
特性 | 传统malloc | 定长内存池 |
---|---|---|
分配速度 | 较慢(系统调用) | 极快(直接操作内存) |
内存碎片 | 可能产生外部碎片 | 无外部碎片 |
系统调用 | 每次分配都可能调用 | 批量申请,极少调用 |
线程安全 | 需要加锁 | 可设计为线程本地 |
适用场景 | 通用分配 | 固定大小对象 |