文章目录
完整代码
项目介绍
当前项目是实现一个高并发的内存池,他的原型是google的一个开源项目tcmalloc,tcmalloc全称Thread-Caching Malloc,即线程缓存的malloc,实现了高效的多线程内存管理,用于替代系统的内存分配相关的函数(malloc、free)。
这个项目是把tcmalloc最核心的框架简化后拿出来,模拟实现出一个自己的高并发内存池,目的就是学习tcamlloc的精华。
内存池
池化技术
所谓“池化技术”,就是程序先向系统申请过量的资源,然后自己管理,以备不时之需。之所以要申请过量的资源,是因为每次申请该资源都有较大的开销,不如提前申请好了,这样使用时就会变得非常快捷,大大提高程序运行效率。
在计算机中,有很多使用“池”这种技术的地方,除了内存池,还有连接池、线程池、对象池等。以服务器上的线程池为例,它的主要思想是:先启动若干数量的线程,让它们处于睡眠状态,当接收到客户端的请求时,唤醒池中某个睡眠的线程,让它来处理客户端的请求,当处理完这个请求,线程又进入睡眠状态。
内存池
内存池是指程序预先从操作系统申请一块足够大内存,此后,当程序中需要申请内存的时候,不是直接向操作系统申请,而是直接从内存池中获取;同理,当程序释放内存的时候,并不真正将内存返回给操作系统,而是返回内存池。当程序退出(或者特定时间)时,内存池才将之前申请的内存真正释放。
内存池主要解决的问题
内存池主要解决的当然是效率的问题,其次如果作为系统的内存分配器的角度,还需要解决一下内存碎片的问题。

上图是外碎片问题。
1、外部碎片是一些空闲的连续内存区域太小,这些内存空间不连续,以至于合计的内存足够,但是不能满足一些的内存分配申请需求。
2、内碎片是由于一些对齐的需求,导致分配出去的空间中一些内存无法被利用。
malloc
C/C++中我们要动态申请内存都是通过malloc去申请内存,但是我们要知道,实际我们不是直接去堆获取内存的,而malloc就是一个内存池。malloc() 相当于向操作系统“批发”了一块较大的内存空间,然后“零售”给程序用。当全部“售完”或程序有大量的内存需求时,再根据实际需求向操作系统“进货”。malloc的实现方式有很多种,一般不同编译器平台用的都是不同的。比如windows的vs系列用的微软自己写的一套,linux gcc用的glibc中的ptmalloc。
开胃菜–先设计一个定长的内存池
先熟悉一下简单内存池是如何控制的,这个定长内存池也会作为后面内存池的一个基础组件
解决固定大小的内存申请释放需求
特点:
1、性能达到机制
2、不考虑内存脆片等问题

首先申请一块大块空间,用_memory指向这块空间。
比如从_memory中分别取出内存块A、内存块B、内存块C使用(形象上说取出,实际地址空间都还是连续的)。当某个内存块使用完后归还时(假设是B,第一次归还),我们用 void* _freeList 指针存B的地址(头结点),后面要归还的内存块再一一往后链接。
每个内存块的前4/8个字节存储要链接在其后面的内存块的地址。
static void*& NextObj(void* obj) //取obj的头4/8个字节,& 也可以写
{
return *((void**)obj);
}
New:从大块空间出取出一块给对象T使用。
不断取出内存块使用,当空间中剩余内存不够一个对象大小时,则丢弃剩余空间,重新开辟新的大块空间。所以需要int _remains变量记录大块空间中剩余的字节数。
如果自由链表中有内存块(_freeList != nullptr),就优先从自由链表中取出内存块使用。如果没有再从大块空间中取。
T* New()
{
T * obj = nullptr;
//如果自由链表有对象,直接取一个
if(_freeList)
{
obj = (T*)_freeList;
_freeList = *((void**)_freeList);
}
else
{
if (_remains < sizeof(T))
{
_remains = 128 * 1024;
//_memory = (char*)malloc(_remains);
_memory = (char*)SystemAlloc(_remains >> 13); //除8k
if (_memory == nullptr)
{
throw std::bad_alloc();
}
}
obj = (T*)_memory;
size_t objSize = sizeof(T) > sizeof(void*) ? sizeof(T) : sizeof(void*);
_memory += objSize;
_remains -= objSize;
}
//对已经有的一块空间初始化,使用定位new显示调用T的构造函数初始化
new(obj)T;
return obj;
}
Delete:当归还一个对象时,将对象(内存块)头插到自由链表中,再次重复利用。
void Delete(T* obj)
{
//显示调用T的析构函数
obj->~T();
//头插
*((void**)obj) = _freeList;
_freeList = obj;
}
注意:如果对象T的大小比指针大小还小,那么从大块空间取内存块时就取指针的大小,方便还回来时连接到一起。
申请内存时,我们可以直接调用系统,找堆按页申请内存,脱离malloc,
这里一页给8K。
//直接去堆上按页申请空间
#ifdef _WIN32
#include <Windows.h>
#else
//...
#endif
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
//kpage*8*1024
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;
}
该定长内存池在接下来的项目中代替new和delete
高并发内存池整体框架设计
现代很多的开发环境都是多核多线程,在申请内存的场景下,必然存在激烈的锁竞争问题。malloc本身其实已经很优秀,项目的原型tcmalloc就是在多线程高并发的场景下更胜一筹,所以实现的内存池需要考虑以下几方面的问题。
- 性能问题
- 多线程环境下,锁竞争问题
- 内存碎片问题
concurrent memory pool主要由以下3个部分构成:

- thread cache: 线程缓存,用于小于256KB的内存的分配。每个线程独享一个thread cache,不需要加锁,这也是这个并发线程池高效的地方
- central cache: 中心缓存,所有线程共享。thread cache按需从central cache中获取对象。central cache在合适时机回收thread cache中的对象,避免一个线程占用太多内存,而其它线程的内存吃紧,达到内存分配在多个线程中更均衡地按需调度的目的。central cache存在竞争,从这里取内存对象时需要加锁,这里用的是桶锁,且只有thread cache没有内存对象时才会找到central cache,所以这里竞争不会很激烈。
- page cache: 页缓存,存储的内存以页为单位存储及分配。当central cache没有内存对象时,从page cache分配出一定数量的page,并切割成定长大小的小块内存,分配给central cache。当一个span的几个跨度页的对象都回收后,page cache会回收central cache满足条件的soan对象,并合并相邻的页,组成更大的页,缓解内存碎片的问题。
thread cache
thread cache是哈希桶结构,每个桶是一个按桶位置映射大小的内存块对象的自由链表。

申请内存:
- 当内存申请size <= 256KB,先获取到线程本地存储的thread cache对象,计算size映射的哈希桶自由链表下标i。
- 如果_freeLists[i]中有对象,则直接Pop一个对象内存返回。
- 如果_freeLists[i]中没有对象时,则批量从central cache中获取一定数量的对象,插到自由链表并返回一个对象
释放内存:
- 当释放内存小于256KB时将内存释放回thread cache,计算size映射自由链表桶位置i,将对象Push到_freeLists[i]。
- 当链表的长度过长,则回收一部分内存对象到central cache。
管理切分好的小对象的自由链表
- Push
头插,取obj对象头上的4个字节(以下表述都假设是32位系统下),指向自由链表的第一个节点,再让obj变为第一个。
void Push(void* obj) //插入一个对象到自由链表
{
assert(obj);
//头插
//*(void**)obj = _freeList;
NextObj(obj) = _freeList;
_freeList = obj;
++_size;
}
- Pop
头删。用obj指向第一个节点,再让_freeList指向obj的下一个节点。
void* Pop()
{
assert(_freeList);
//头删
void* obj = _freeList;
_freeList = NextObj(obj);
--_size;
return obj;
}
计算对象大小的对齐映射规则
当一个对象被切小了挂到自由链表中时,64位下至少也要8字节来存储地址,所以这里刚开始以8字节对齐。但所有都以8字节对齐的话需要建的自由链表太多了,这里用一种规则来对齐。(向上对齐,但会有内碎片浪费)
class SizeClass
{
public: // 整体控制在最多10%左右的内碎片浪费
// [1,128] 8byte对齐 freelist[0,16)
// [128+1,1024] 16byte对齐 freelist[16,72)
// [1024+1,8*1024] 128byte对齐 freelist[72,128)
// [8*1024+1,64*1024] 1024byte对齐 freelist[128,184)
// [64*1024+1,256*1024] 8*1024byte对齐 freelist[184,208)
//移位运算比加减乘除效率高
static inline size_t _RoundUp(size_t bytes, size_t alignNum)
{
return ((bytes + alignNum - 1) & ~(alignNum - 1));
}
static inline size_t RoundUp(size_t size) //对齐以后是多少
{
if (size <= 128)
{
return _RoundUp(size, 8);
}
else if (size <= 1024)
{
return _RoundUp(size, 16);
}
else if (size <= 8 * 1024)
{
return _RoundUp(size, 128);
}
else if (size <= 64 * 1024)
{
return _RoundUp(size, 1024);
}
else if (size <= 256 * 1024)
{
return _RoundUp(size, 8 * 1024);
}
else
{
return _RoundUp(size, 1 << PAGE_SHIFT);
}
}
比如说需要129字节的空间,在[128+1, 1024]范围内是按16字节对齐,15÷144=10%左右,即有10%左右的内碎片浪费。
计算映射的哪一个自由链表桶
//size_t _Index(size_t bytes, size_t alignNum)
//{
// if (bytes % alignNum == 0)
// {
// return bytes / alignNum - 1;
// }
// else
// {
// return bytes / alignNum;
// }
//}
// 1 << 3 = 8
static inline size_t _Index(size_t bytes, size_t align_shift)
{
return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;
}
//计算映射的哪一个自由链表桶
static inline size_t

本文详细介绍了如何设计一个高并发内存池,以tcmalloc为原型,涵盖定长内存池、线程缓存、中心缓存和页缓存的实现,以及如何通过基数树优化内存分配。学习了内存池管理、多线程同步和内存碎片处理的关键技术。
最低0.47元/天 解锁文章
3万+

被折叠的 条评论
为什么被折叠?



