高并发内存池

针对多线程环境下小内存块频繁申请和释放导致的效率降低和内存碎片问题,本文提出了一种高并发内存池设计方案。通过线程私有的thread_cache和公共的central_cache,结合哈希桶和内存分块管理,实现高效内存分配和回收。同时,pagecache层负责向系统申请内存并管理大块内存的合并与分割。项目实验证明该设计有效。
摘要由CSDN通过智能技术生成

实际问题

在多个线程频繁进行小块内存的申请和释放的情况,多个进程向内核申请内存,带来的进程之间激烈竞争,大大减少了工作效率,同时小块内存带来的内存碎片问题会降低内存的使用效率。

设计思路

1.每个线程有自己独立的一块内存,小的内存块到这上面去切。

2.线程需求的内存块大小不同,为其划分不同的大小内存块,这些内存块是切好的自由链表,并且按照哈希桶的方式存储,这样线程thread_cache就能轻松的为线程获取对应大小的内存块。每个线程都有其私有的thread_cache

3.在thread_cache之上有公有的central_cache,central_cache同样是哈希桶结构,每个桶里有一块或几块固定大小的内存块,这些内存块有在其内被切成自由链表,thread没有内存向central申请,thread闲置内存过多,向central退还。 central在进程内只有唯一一份。

4.pagecache,最底层,同样是哈希桶结构,负责向系统申请内存,并将申请到的指针转化为页号(64和32转化不同),同时负责向central发放内存,并回收小块的页将其合并为大块内存。

基础结构见下图(手绘拙劣)

设计主体

1.定长内存池+线程池

用于摆脱系统申请malloc和缓解批量线程问题。

定长内存池,线程池实现方法很多就不赘述了。

class ObjectPool {
public:

  //内存申请
	T* New()
	{
		//返回的指针
		T* obj = nullptr;

		//判断自由链表是否为空
		if (_freeList)
		{
			void* next = *((void**)_freeList);
			obj = (T*)_freeList;
			_freeList = next;
		}
		else {
		//申请空间,并且一小块一小块的给
		if (_remain < sizeof(T))
		{
			_remain = 1024 * 128;
        /* _memory = (char*)malloc(1024 * 128);通过malloc内存池申请空间*/
		    _memory = (char*)SystemAlloc(_remain >> 13);//直接向堆申请空间
		 
		 if (_memory == nullptr)
		 {
			 printf("New error\n");
			 throw std::bad_alloc();
		 }
		}
		obj = (T*)_memory;
		size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
		_memory += objSize;
		_remain -= objSize;
		}

		new(obj)T;
		return obj;
    }

  void Delete(T*obj)
  {
   //内存返回
	  obj->~T();
   //内存放进自由队列,模板本身自己析构。
	  *(void**)obj = _freeList;
	  _freeList = obj;
  }
private:
	//大块内存,开始时的头指针
	char* _memory = nullptr;
	//剩余多少内存可用
	size_t _remain = 0;
	//自由链表,自由链表的起始者
	void* _freeList = nullptr;
};

2.thread cache


class ThreadCache
{
public:

	void* Allocate(size_t size);
	

	void  Delallocate(void*ptr,size_t size);
	
	//从CentralCache申请内存
	void* FetchFromCentralCache(size_t index, size_t size);

	void ListTooLong(FreeList& List, size_t size);

private:
	FreeList _freeLists[NFREELIST];
	 
};

static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;


class FreeList
{
private:
	size_t _listLength = 0;
	size_t _maxSize = 1;
	void* _freeList = nullptr;
};

From是从central获取span中切分好的自由链表,然后内存被使用完后返回到_freeLists[NFREELIST]中的哈希桶。

     TooLong 是返回的自由链表过长后将内存返回给central。

     这里的内存申请采用了慢增长策略,并非一次给最大值的数量的小内存块,并且此慢增长机制同样用于什么时候还内存给central。

//先确定申请多少,慢启动策略。
	size_t batchNum = min(_freeLists[index].MaxSize(), SizeClass::NumFromCentral(size));
	
	//每次多申请一点点。
	if (batchNum == _freeLists[index].MaxSize())
	{
		_freeLists[index].MaxSize() += 1;
	}

随着MaxSize慢慢变大一次能申请的小块内存才增多。

在返回时也是挂在thread_cache中哈希桶的自由链表长度超过MaxSize就还内存给central

3.central_cache

这是承上启下的一块内容,这里要全局只有一个对象,故采用单例模式进行设计。

class CentralCache 
{
public:
	//获得单例模式对象
	static CentralCache& GetCenObject()
	{
		return _sInst;
	}

	//向pagecache申请span  
	Span* ApplyNewSpan(SpanList& list,size_t size);
	
	//向threadcache对应桶的自由链表注意加锁 
	size_t ApplyCentralList(size_t size, size_t batchNum, void*& start, void*& end);
	
	//过长的_freelist归还回来
	void GiveBackToCentral(void* start, size_t size);
private:
    SpanList _spanLists[NFREELIST];

	//单例模式
	CentralCache() {}
	CentralCache(const CentralCache&) = delete;

	static CentralCache _sInst;
};	

struct Span
{
	int _pageId = 0;
	int _pageSpan = 0;

	void* _freelist = nullptr ;
	int _useCount = 0;
	bool _isUse = false;
	int _sizeObj = 0;

	Span* _prev = nullptr;
	Span* _cur = nullptr;
};

class SpanList
{
public:
	std::mutex _mtx;
//增删......

private:
	Span* _head;
	
};

所有线程都要访问central故哈希桶中需要加桶锁,防止线程冲突。当对应的哈希桶中没有span时向page_cache申请,申请回来之后要对其进行自由链表的划分处理,当span中的usecount归零时证明这一整块内存都没使用要返回给pagecache。

4.page_cache


class PageCache 
{
public:

	//获得单例模式对象
	static PageCache& GetPageObject()
	{
		return _pInst;
	}

	//Cen向page申请span
	Span* ApplyPageSpan(size_t size);

	//获得页和span的映射关系
	Span* ApplyIdToSpan(void*ptr );

	void GiveBackToPage(Span* span);

	std::mutex _pageMtx;
private:
	ObjectPool<Span> _spanPool;
	/*std::unordered_map<PAGE_ID, Span*> _idToSpan;用基数树性能优化*/
	TCMalloc_PageMap1<32 - PAGE_SHIFT> _idToSpan;

	SpanList _spanLists[NPAGES];
	static PageCache _pInst;
	PageCache() {}
	PageCache(const PageCache&) = delete;
};

这里也使用了单例模式。申请的内存指针地址采用了基数树进行映射,建立起对应关系。

当central申请内存时,从page对应(映射)桶取span,没有就向page下一个桶取,取到了大的span将其进行切分,一份给申请的central桶,一份挂到page小桶中。直到最后的桶中也没有128页的span,就向系统申请128页的内存(8k一页),进行切分。

当central中span还到page时,会检查有没有和个span相邻页数的span,有就进行合并,直到没有相邻页或者长度会超过128页就停止。

这里在申请大块内存时,使用了复用机制,避免了再次写切分代码的冗余。

//走到这里就要向底层申请128页的大页内存,并回调一次
		Span* maxSpan = _spanPool.New();;
		maxSpan->_pageSpan = NPAGES - 1;
	
		//申请内存
		void* ptr = SystemAlloc(NPAGES - 1);
		maxSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
		
		_spanLists[NPAGES - 1].PushFront(maxSpan);

		return ApplyPageSpan(k);

项目效果

项目总结 

实际上这个项目还有很多细节需要控制,我在调试时遇到了很多次野指针问题,非常难调试,而且不一定每次调试都会出现错误,其中有一次就是合并之后没有挂回page。

具体代码见此:https://gitee.com/bfzxie/high-concurrency-memory-pool/tree/master/ConcurrentMemoryPool

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值