什么是高并发内存池?
内存池(Memory Pool) 是一种动态内存分配与管理技术。 通常情况下,程序员习惯直接使用 new、delete、malloc、free 等API申请分配和释放内存,这样导致的后果是:当程序长时间运行时,由于所申请内存块的大小不定,频繁使用时会造成大量的内存碎片从而降低程序和操作系统的性能。内存池则是在真正使用内存之前,先申请分配一大块内存(内存池)留作备用,当程序员申请内存时,从池中取出一块动态分配,当程序员释放内存时,将释放的内存再放入池内,再次申请池可以 再取出来使用,并尽量与周边的空闲内存块合并。若内存池不够时,则自动扩大内存池,从操作系统中申请更大的内存池。
项目介绍:
本项目参考了谷歌 tcmalloc 设计模式,设计实现了高并发的内存池。基于 win10 环境 VS2022,采用 C++进行编程,池化技术、多线程、TLS、单例模式、互斥锁、链表、哈希等数据结构。该项目利用了 thread cache、central、cache、page cache 三级缓存结构,基于多线程申请释放内存的场景,最大程度提高了效率,解决了绝大部分内存碎片问题。
项目目的:
现代很多的开发环境都是多核多线程,在申请内存的场景下,必然存在激烈的锁竞争问题。所以这次我们实现的内存池主要解决的就是效率的问题,它能够避免让程序频繁的向系统申请和释放内存。其次,内存池作为系统的内存分配器,还需要尝试解决内存碎片的问题。
什么是池化技术?
池是在计算机技术中经常使用的一种设计模式,其内涵在于:将程序中需要经常使用的核心资源
先申请出来,放到一个池内,由程序自己管理,这样可以提高资源的使用效率,也可以保证本程
序占有的资源数量。 经常使用的池技术包括内存池、线程池和连接池等,其中尤以内存池和线程
池使用最多。
项目结构:
主要由线程缓存(threadcache)、中心缓存(centralcache)、页缓存(pagecache)3个部分构成。
线程缓存:
为了保证效率,我们使用线程局部存储(thread local storage,TLS)技术保存每个线程本地的ThreadCache的指针,这样大部分情况下申请释放内存是不需要锁的,线程缓存是每个线程独有的,用于小于64k的内存的分配,但并不是一定是要64k,只是前人总结的一个合适值。线程从这里申请内存不需要加锁,每个线程独享一个cache,这也就是这个并发线程池高效的地方。
class ThreadCache
{
public:
void* Allocate(size_t size);
void Deallocate(void* ptr, size_t size);
void* FetchFromCentralCache(size_t index, size_t size);
void ListTooLong(FreeList& list, size_t size);
private:
FreeList _freeLists[NFREELISTS];
};
static __declspec(thread) ThreadCache* tls_threadcache = nullptr;
中心缓存(centralcache):
中心缓存是所有线程所共享,本质是由一个哈希映射的span对象自由链表构成thread cache是按需从central cache中获取的对象。central cache周期性的回收thread cache中的对象,避免一个线程占用了太多的内存,而其他线程的内存吃紧。达到内存分配在多个线程中更均衡的按需调度的目的。central cache是存在竞争的,所以从这里取内存对象是需要加锁,不过一般情况下在这里取内存对象的效率非常高,所以这里竞争不会很激烈。
class CentralCache
{
public:
//唯一获得该对象的接口
static CentralCache* GetInstance()
{
return &_sInst;
}
// 从中心缓存获取一定数量的对象给thread cache
size_t FetchRangeObj(void*& start, void*& end, size_t n, size_t byte_size);
Span* GetOneSpan(SpanList& list, size_t byte_size);
void ReleaseListToSpans(void* start, size_t byte_size);
private:
SpanList _spanLists[NFREELISTS]; // 按对齐方式映射
private:
CentralCache()
{}
CentralCache(const CentralCache&) = delete;
CentralCache& operator=(const CentralCache&) = delete;
//定义全局唯一类
static CentralCache _sInst;
};
页缓存(pagecache):
页缓存是在central cache缓存上面的一层缓存,存储的内存是以页为单位存储及分配的,central cache没有内存对象时,从page cache分配出一定数量的page,并切割成定长大小的小块内存,分配给central cache。page cache会回收central cache满足条件的span对象,并且合并相邻的页,组成更大的页,缓解内存碎片的问题。
class PageCache
{
public:
static PageCache* GetInstance()
{
return &_sInst;
}
// 向系统申请k页内存挂到自由链表
void* SystemAllocPage(size_t k);
Span* NewSpan(size_t k);
// 获取从对象到span的映射
Span* MapObjectToSpan(void* obj);
// 释放空闲span回到Pagecache,并合并相邻的span
void ReleaseSpanToPageCache(Span* span);
private:
SpanList _spanList[NPAGES]; // 按页数映射
//std::mutex _map_mtx; //专门给map用的锁
std::unordered_map<PageID, Span*> _idSpanMap;
std::recursive_mutex _mtx;
private:
PageCache()
{}
PageCache(const PageCache&) = delete;
PageCache& operator=(const PageCache&) = delete;
// 单例
static PageCache _sInst;
};
加锁场景:
1.在centralcache结构中需要进行加桶锁,也就是给FetchRangeObj函数和ReleseListToSpans函数进行加。
2.在pagecache结构中加大锁,也就是NewSpan函数和ReleaseSpanToPageCache函数进行加,还有就是在对span地址和页号映射时需要的MapObjectToSpan函数进行加。
总结
该项目将申请内存分为了三个层,并采取了基数树和定长内存池进行优化。项目总体难度适中,代码量不多,但理解起来比较难,如有错误请您指正,谢谢。