一、项目介绍
简化版高并发内存池是基于Google 的一个开源项目TCMalloc,TCMalloc 是 Google 开发的内存分配器,全称为Thread-Caching-Malloc,即线程缓存的malloc,实现高效的多线程内存管理。
1.1、项目知识要求
这个项目涉及C/C++、数据结构(链表、哈希桶)、操作系统的内存管理、单例模式、多线程、互斥锁等。
二、内存池
2.1、池化技术
所谓的**“池化技术”**就是向系统申请一个过量的资源,然后自己管理,类似的现象就是比如向家里人要生活费,索要生活费可大致分为两种,一种就是每天需要多少生活费,就问家里人要多少生活费,另外一种就是,一次要一周的生活费自己管理。池化技术就类似后者,可以避免每次都问系统申请资源,提高程序运行效率。
2.2、内存池
内存池百度百科
内存池就是指程序先从操作系统中申请一块足够大的内存,此后,当程序需要申请内存时,不是直接向操作系统申请,而是直接从内存池中获取;同理,当程序释放内存时,并不是直接将内存返回给操作系统,而是返回内存池。当程序退出时,内存池将之前申请的内存释放。
2.3、内存池主要解决什么问题
1、效率问题
针对效率问题,如前面说的生活费问题,可以避免多次向操作系统申内存。
2、内存碎片的问题
a、内存碎片是怎么产生的?
如上图所示,vector向系统申请了256Byte的空间,然后又释放给系统,list向系统申请128Byte的空间,然后也释放了,我们现在又三百多Byte的空间,现在想申请三百多Byte的空间,但是会申请失败,因为系统中的那三百多Byte的空间是不连续的,所以哪些空间就成为了内存碎片。
三、定长内存池
定长内存池就是空间大小固定的内存。
定长内存池之所以高效,是因为它可以切除固定大小的内存供线程使用。
3.1如何设计一个定长内存池?
3.1.1定长内存池中有什么?
定长内存池包括:一个大块内存(内存池),一个用于链接释放空间的自由链表,用于计算大块内存在切分后剩余空间大小。
private:
//指向大块内存的指针
char* _memory=nullptr;//为什么要用char?因为一个char就是一个在字节
void* _freeList = nullptr;//还回过程中链接的自由链表的指针
size_t _remainBytes = 0;//大块内存剩余的字节数
3.1.2创建内存池
首先直接向系统申请一大块内存,用来做内存池
申请完后就可以使用该内存池,当我们释放内存时,该内存就会被挂在自由链表中(回收内存链表),所以,我们应该首先判断自由链表中有没有已经回收了的内存,如果有,我们优先使用该链表中挂的内存,否则直接在内存池中申请内存。
T* New()
{
T* obj;
//优先使用_freelist中的空间
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();
}
/*T* obj = (T*)_memory;
size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
_memory += objSize;
_remainBytes -= sizeof(T);*/
}
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;
}
3.1.3释放内存
当内存不用之后,直接用链表将释放的内存块挂起来
我们这里将第一块内存中的一部分用来存放下一内存的地址,这样就可以像链表一样将内存块挂起来
//释放内存
void Delete(T* obj)
{
//显示调用析构函数
obj->~T();
//if (_freeList == nullptr)
//{
// _freeList = obj;
// //*(int*) 对int*解引用就会指向四个字节
// //*(void**)在32或者64位下分别是4字节和8字节,刚好可以存一个地址
// *(void**)obj = nullptr;//首先先让这个返回的自由链表的前四个字节指向nullptr
//}
//else
//{
// *(void**)obj == _freeList;
// _freeList = obj;
//}
//直接头插就可以,不需要去判断是否为空
*(void**)obj = _freeList;
_freeList = obj;
}
3.2整体代码
#pragma once
#include"Common.h"
//定长内存池
template<class T>
class ObjectPool
{
public:
//申请内存
T* New()
{
T* obj;
//优先使用_freelist中的空间
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();
}
/*T* obj = (T*)_memory;
size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
_memory += objSize;
_remainBytes -= sizeof(T);*/
}
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();
//if (_freeList == nullptr)
//{
// _freeList = obj;
// //*(int*) 对int*解引用就会指向四个字节
// //*(void**)在32或者64位下分别是4字节和8字节,刚好可以存一个地址
// *(void**)obj = nullptr;//首先先让这个返回的自由链表的前四个字节指向nullptr
//}
//else
//{
// *(void**)obj == _freeList;
// _freeList = obj;
//}
//直接头插就可以,不需要去判断是否为空
*(void**)obj = _freeList;
_freeList = obj;
}
private:
//指向大块内存的指针
char* _memory=nullptr;//为什么要用char?因为一个char就是一个在字节
void* _freeList = nullptr;//还回过程中链接的自由链表的指针
size_t _remainBytes = 0;//大块内存剩余的字节数
};
struct TreeNode
{
int _val;
TreeNode* _left;
TreeNode* _right;
TreeNode()
:_val(0)
, _left(nullptr)
, _right(nullptr)
{
}
};
//测试用例
void TestObjectPool()
{
//申请释放的轮次
const size_t Rounds = 5;
//每轮申请释放多少次
const size_t N = 100000;
std::vector<TreeNode*> v1;
v1.reserve(N);
size_t begin1 = clock();
for (size_t j0 = 0; j0 < Rounds; ++j)
{
for (int i = 0; i < N; ++i)
{
v1.push_back(new TreeNode);
}
for (int i = 0; i < N; ++i)
{
delete v1[i];
}
}
size_t end1 = clock();
std::vector<TreeNode*> v2;
v2.reserve(N);
ObjectPool<TreeNode> TNPool;
size_t begin2 = clock();
for (size_t j = 0; j < Rounds; ++j)
{
for (int i = 0; i < N; ++i)
{
v2.push_back(TNPool.New());
}
for (int i = 0; i < N; ++i)
{
TNPool.Delete(v2[i]);
}
v2.clear();
}
size_t end2 = clock();
cout << "new cost toime:" << end1 - begin1 << endl;
cout << "object pool cost time:" << end2 - begin2 << endl;
}
3.2.1测试结果
四、高并发内存池整体结构框架设计
在实现内存池时,我们一般需要考虑到效率问题和内存碎片问题,但是对于高并发内存池来说,我们不仅需要考虑以上问题,还需要考虑在多线程环境下的锁竞争问题
高并发内存池整体框架由以下几个部分组成:thread cache(线程缓存)、central cache(中心缓存)、page cache(页缓存)。
4.0各个部分的主要作用
- thread cache主要解决的是锁竞争的问题,每一个线程独享自己的thread cache,当thread cache中有内存时,该线程就不会和其他线程进行竞争,每个线程只需要在自己的thread cache中申请内存就好。
- central cache主要起到一个居中调度的作用,每一个线程的thread cache需要从central cache中获取内存,如果thread cache中的内存有很多的时候,就会将内存还给central cache,其作用类似于中枢,起到调节的作用,所以被称为中心缓存。
- page cache就负责提供以页为单位的大块内存,当central cache需要内存时,就会向page cache申请,而当page cache中没有足够多的内存时,就会直接向系统进行申请。
4.1thread cache(线程缓存)
a、线程缓存就是每个线程独有一个线程缓存空间,用于小于256KB的内存的分配,线程从这里申请内存不需要加锁,这样不经可以保证线程与线程之间的独立,并且可以保证并发高效。
b、定长内存池只支持固定大小的内存块的申请,因此定长内存池只需要一个自由链表用来管理释放回来的内存块(固定大小的内存块)。如果我们想要申请和释放不同大小的内存块,那么就需要多个自由链表来管理释放回来的内存块,因此thread cache实际上是一个哈希桶的结构,每个桶上挂着不同大小的自由链表。
c、我们需要设计所有大小的内存块吗?不是的,如果我们要设计所有大小的内存块,我们就需要20多万个自由链表,这样无疑是一个很大的工程,并且只是用来存储这些自由链表的头指针就要消耗大量的内存。所有我们采用按照某种规则对这些字节数进行对齐,这里我们采用的是8字节对齐规则(因为在64位下,8字节刚好可以存储头指针),比如我们申请1~8字节大小的内存时,thread cache 直接就给我们8字节,如果申请9~16字节时,就直接给我们16字节大小的内存块。
但是当在多线程情况下,thread cache 可能会同时去central cache 申请内存,此时就会涉及线程安全的问题,因此在访问central cache时需要加锁,但是central cache实际上是一个哈希桶的结构,只有当多个线程同时访问一个桶的时候才会加锁,所以这里的锁竞争问题不是很激烈。
4.1.1thread cache 包含的内容
a、插入:push---------》头插
void push(void* obj)
{
//头插
//*(void**)obj = _freeList;//先将obj强转成void**,这样就会取到前四或者八个个字节(地址)
NextObj(obj) = _freeList;
_freeList = obj;
++_size;
}
b、删除(弹出):pop--------》头删
void* pop()
{
//头删
assert(_freeList);
void* obj = _freeList;
_freeList = NextObj(obj);
--_size;
return obj;
}
threadcache哈希桶映射对齐规则
虽然对齐产生的内碎片会引起一定程度的空间浪费,但按照上面的对齐规则,我们可以将浪费率控制到百分之十左右。需要说明的是,1~128这个区间我们不做讨论,因为1字节就算是对齐到2字节也有百分之五十的浪费率。
对齐和映射相关函数的编写:
我们需要提供两个对应的函数,分别用于获取某个大小字节数对齐后的字节数,以及该字节数对用的哈希桶的下标(当释放该内存时,方便将该内存块归还)
//管理对齐和映射等关系
class SizeClass
{
public:
//获取向上对齐后的字节数
static inline size_t RoundUp(size_t bytes);
//获取对应哈希桶的下标
static inline size_t Index(size_t bytes);
};
获取对齐后的字节数:我们需要先判断该申请的内存块位于哪一个区间,然后再通过调用子函数进行进一步的处理。
//获取向上对齐后的字节数
static inline size_t RoundUp(size_t bytes)
{
if (bytes <= 128)
{
return _RoundUp(bytes, 8);
}
else if (bytes <= 1024)
{
return _RoundUp(bytes, 16);
}
else if (bytes <= 8 * 1024)
{
return _RoundUp(bytes, 128);