基于C++的高并发内存池

内存池

内存池是一种动态分配与管理技术。当我们使用new/delete 或者malloc/free的时候,系统都会在堆上分配相应的内存给我们。当我们频繁的去申请内存释放内存时,就可能会造成内存碎片的问题。所以内存池会把我们可能会用到的资源先申请出来,放在一个池内管理,这样可以提高资源的使用效率,也可以让每个线程都有相应的资源数量。当我们申请资源时,直接在内存池中获取,释放资源时将资源放进池中。

内存碎片问题

如果我们频繁的去申请内存、释放内存时,就会让一段原本连续的内存被切成很多细小的碎片。例如我们向图中这样申请释放内存。
在这里插入图片描述
当我们释放完内存后,我们一共有8+8+4=20kb内存可以使用,但我们却不能在申请出一块连续的16kb的内存。这就是内存碎片的问题。尽管看起来我们有很多内存。却不能很好的使用。这个问题叫做“外碎片”。
那么相对的就会有内碎片问题。 我们再向系统申请内存时,假如我们需要三字节。但是系统为了方便管理。他并不会真的给我们三字节的内存。他可能会给我们四字节或者八字节。那我们却只是用三字节内存。那么剩余的部分也会被浪费掉。这种内存碎片我们称为“内碎片”。

申请效率的问题

因为我们申请内存是需要去调用系统IO的,而每次我们都需要从用户态切换到内核态,还需要程序上下文的切换,这些都是很耗费资源的。所以如果我们使用内存池,就可以先申请一批内存,当我们需要使用的时候,在自己去分配已经拿到的那部分内存。就会提高我们的申请效率。

malloc概述

其实malloc也有一个内存池结构。malloc在底层使用了分离适配。 分配器维护了一个空闲链表数组。每个空闲链表包含大小不同的内存块。当要分配一个内存时,我们对相应的空闲聊表查找合适的内存块。如果没有相应大小的内存块了,则在更大的内存块对应链表进行搜索,找到合适的进行切割处理。 如果所有的空闲链表都没有找到。则向操作系统申请一块大内存,然后在新的内存中分配一块。
在释放内存时,则将内存重新放回相应的空闲链表中。

并发内存池的设计概述

因为我们很多开发都时需要用多线程,高并发的情况。那这种时候我们就需要更多的去考虑线程安全,资源竞争的问题。所以我们除了再考虑到内存碎片的处理问题和性能外,还需要考虑再多核多线程的环境下,考虑到锁竞争时,如何让效率更快一点。
在考虑到这些情况下,我们的内存池主要有三个部分构成:

  • thread cache :线程缓存是每个线程独立占有的,当我们申请一些小内存时,线程就可以从这里申请内存使用,这样就不需要加锁。因为每个线程都可以独享一个cache ,这样就会让申请效率提高,同时不用担心锁竞争问题。
  • central cache:中心缓存是所有线程共享的资源,thread cache从这里获取内存块。central cache负责回收从thread cache中被释放的大量内存,然后可以将这些内存分配给其他的线程,这样可以避免一个线程占用太多的内存资源。
  • page cache 页缓存是central cache的上层缓存,负责管理以页为单位的大块内存,当central cache没有内存资源时,从page cache中划分资源交给central cache 。在页缓存中,我们还需要将切碎但是释放的内存合并,组成更大的内存,这样就可以有效的缓解内存碎片的问题。

在这里插入图片描述

common类

在整个结构中,我们需要一些共通的数据结构来帮助我们管理和分配内存。例如确定对应size大小内存块的使用情况的span,管理内存分配的自由链表freelist,以及我们还需要记录每个块内存对应的span,管理他们的映射关系。

自由链表freelist

自由链表是我们用来管理不同大小内存块和span的主要数据结构,我们需要用自由链表来管理各个线程的私有缓存。通过自由链表和哈希映射,我们也能较高效率的拿到我们想要大小的内存块。

class FreeList
{
public:
	void Push(void* obj) {
		NextObj(obj) = _head;
		_head = obj;
		++_size;
	}
	void* Pop() {
		void* obj = _head;
		_head = NextObj(_head);
		--_size;
		return obj;
	}

	/// <summary>
	/// 插入n个
	/// </summary>
	/// <param name="start"></param>
	/// <param name="end"></param>
	/// <param name="n"></param>
	void PushRange(void* start, void* end,size_t n) {
		NextObj(end) = _head;
		_head = start;
		_size += n;
	}
	 
	/// <summary>
	/// 将n个拿出来
	/// </summary>
	/// <param name="start"></param>
	/// <param name="end"></param>
	/// <param name="n"></param>
	void PopRange(void*& start,void*& end ,int n) {
		
		start = _head;

		for (int i = 0; i < n;++i) {
			end = _head;
			_head = NextObj(_head);
		}
		NextObj(end) = nullptr;
		_size -= n;
	}


	bool Empty() {
		return _head == nullptr;
	}
	size_t Size() {
		return _size;
	}
	size_t MaxSize() {
		return _max_size;
	}
	void SetMaxSize(size_t size){
		_max_size = size;
	}
private:
	void* _head = nullptr;
	size_t _size = 0;
	//要对象的频率    
	size_t _max_size = 1;
};
内存管理span

span则是我们管理批量内存块的数据结构,

threadcache

thread cache为每个线程私有,在线程缓存中我们维护一个自由链表,来存放当前线程可用的内存块。当我们申请小内存时。找到对应size大小的自由链表,从自由链表中拿取一个内存块使用,这样也没有锁竞争。 当自由链表中没有对象时,则批量从central cache中获取一定数量的对象,插入到自由链表中,然后取一个对象线程用。
在这里插入图片描述

class ThreadCache
{
public:
	/// <summary>
	/// 申请内存大小
	/// </summary>
	/// <param name="bytesize"></param>  申请size大小内存
	/// <returns></returns>  返回指向地址的指针
	void* Allocate(size_t bytesize);


	/// <summary>
	/// 释放内存
	/// </summary>
	/// <param name="bytesize"></param> 释放大小
	void Deallocate(void* ptr,size_t bytesize);


	/// <summary>
	/// 从中心缓存获取对象
	/// </summary>
	/// <param name="index"></param> 获取对象放在哪个链表
	/// <param name="bytesize"></param>  获取对象大小
	void* FetchFromCentralCache(size_t index, size_t bytesize);

	/// <summary>
	/// 释放对象时,回收长链表至中心缓存
	/// </summary>
	/// <param name="list"></param>
	/// <param name="size"></param>
	void ListTooLong(FreeList& list, size_t bytesize);

private:
	FreeList _freelist[NFREELIST];
};
TLS

TLS (thread local storage)可以保存每个线程本地的threadcache指针,用tls的变量会使每个线程单独保存一份自己的指针,当线程需要访问这个变量时,会通过这个指针去访问,而每个线程的指针只向自己的这个数据。所以即使不上锁,各个线程也不会引起线程安全问题。
同时,TLS分为静态和动态的。我们这里可以通过定义一个静态的TLS就可以实现我们的需求。

static _declspec(thread) ThreadCache* tls_threadcache = nullptr; 

centralcache

当thread cache中没有内存时,就会批量向central cache申请一些内存对象,central cache 也有一个哈希映射的自由链表, 只不过这里的自由链表不是挂着的不是内存块,而是一个完整span,一个span会完整的管理数页大小的内存。 从对应size的链表中找一个可用的span,获取一部分内存块分给thread cache。
当central cache中没有找到合适的非空的span时,则再向上找page cache申请一个span对象。切成对应size的大小块,并链接起来,挂到span中。最后交给thread cache。

在释放内存时,当thread cache过长或者释放线程后,我们需要将thread cache中占用的内存资源回收进central cache中,释放回来时,我们统计各个span内的使用个数相应减少。当一个span的使用个数为零时,则表示这个span内所有对象都全部释放了。这样就可以将这个span合并会page cache ,以便于更大内存的合并和申请。
在这里插入图片描述


typedef size_t pageID;

class  CentralCache
{
public:
	/// <summary>
	/// 从中心缓存中获取一定范围对象对象
	/// </summary>
	/// <param name="start"></param>  获取内存起始位置
	/// <param name="end"></param>	 获取内存结束位置
	/// <param name="n"></param>  对象个数
	/// <param name="bytesize"></param> 单个对象大小
	/// <returns></returns>
	size_t FetchRangeObj(void*& start, void*& end, size_t n, size_t bytesize);

	/// <summary>
	/// 向pagecache获取内存
	/// </summary>
	/// <param name="list"></param> 获取内存挂在list上
	/// <param name="bytesize"></param>  获取内存的大小
	/// <returns></returns>
	Span* GetOneSpan(SpanList& list, size_t bytesize);

	/// <summary>
	/// 将一定数量的对象放回对象大小span  
	/// </summary>
	/// <param name="start"></param>
	/// <param name="byte_size"></param>
	void ReleaseListToSpan(void* start, size_t byte_size);

	static  CentralCache* GetCentralCache() {
		return &centralcache;
	}
private:

	CentralCache() = default;
	CentralCache(const CentralCache&) = delete;
	CentralCache(const CentralCache*) = delete;

	SpanList _spanlist[NFREELIST];


	static CentralCache centralcache;
};

在central cache中,我们使用了单例模式,因为一个多个线程需要公用一个central cache ,这样central cache才能起到调度各个线程占用资源的比例。所以在上面的声明中,我们将central cache的几个构造函数私有并加上了delete关键词。

pagecache

pagecache是一个以页单位的span自由链表,为了保证全局唯一性,在pagecache中也使用了单例模式。
当central cache向page cache申请内存时,page cache先检查对应位置有没有span,如果没有则向更大页寻找一个span,如果找到则分裂成两个,一个为我们需要大小的对应的span,剩下一个span则挂在对应位置。 如果最大位置(128page)都没有找到span,则向系统申请一个大片内存(128页)每次申请都申请最大值(128),从而尽量减少去向系统申请内存的次数。 申请到一个大片内存后,再重新寻找合适的span。
如果central cache释放回一个span,则一次寻找span的前后page id的span,如果前面或者后面对应的span也没有被使用,则可以合并出一个更大的span,如果合并成功则继续向前向后合并,这样就可以减少内存碎片。
在这里插入图片描述



class PageCache
{
public:
	/// <summary>
	/// 获取一片napge页内存
	/// </summary>
	/// <param name="npage"></param>
	/// <returns></returns>
	Span* NewSpan(size_t npage);
	//Span* _NewSpan(size_t npage);

	/// <summary>
	/// 向系统要内存
	/// </summary>
	/// <param name="npage"></param>
	void* SystemAllocPage(size_t npage);

	/// <summary>
	/// 获取从对象到span的映射关系
	/// </summary>
	/// <param name="obj"></param>
	/// <returns></returns>
	Span* MapObjectToSpan(void* obj);

	/// <summary>
	/// 返回id对应span的bytesize
	/// </summary>
	/// <param name="id"></param>
	/// <returns></returns>
	size_t GetIdToSize(pageID id);
	/// <summary>
	/// 设置id对应span的切割bytesize
	/// </summary>
	/// <param name="id"></param>
	/// <param name="bytesize"></param>
	/// <returns></returns>
	void SetIdToSize(pageID id, size_t bytesize);

	/// <summary>
	/// 向pagecache合并 前后检查是否可以合并出更大的空间
	/// </summary>
	/// <param name="span"></param>
	void ReleaseSpanToPageCache(Span* span);

	static PageCache* GetPageCache() {
		return &pagecache;
	}
	
private:
	PageCache() = default;
	PageCache(const PageCache&) = delete;
	PageCache(const PageCache*) = delete;
	/// <summary>
	///  按页数映射的spanlist
	/// </summary>
	SpanList _spanlist[NPAGES];
	
	
	//  基数树
	TCMalloc_PageMap1<32 - PAGE_SHIFT> _idSizeMap;
	TCMalloc_PageMap2<32 - PAGE_SHIFT> _idSpanMap;
	//std::unordered_map<pageID, Span*> _idSpanMap;
	
	static PageCache pagecache;
public:

	std::recursive_mutex _mtx;
};
基数树

在我们上面的pagecache中,为了保存内存块和span之间的映射关系,我们使用了TCMallocPageMap,他的底层是基数树,在一般情况下,我们也可以利用STL库中的unordered_map来保存这些映射关系,但是在pagecache中,我们每次去改变这些映射关系时,有可能会有其他线程来访问这些映射关系,如果我们使用unordered_map时,为了线程安全,我们需要对pagecache加锁。以免出现冲突。
但是通过基数树,我们将所有的内存块分开放在一个Sizemap和Spanmap中来保存内存块与span之间的映射关系。 因为在基数树中,每块内存都有一个位置来存放相应的映射关系

  • 当我们需要修改这些映射关系时,有可能是在一片大内存被分配切割时,这时我们需要将这部分内存提交给centralcache,所以这时候是不会有别的线程来访问这个内存块对应的映射关系的,因为我们还没有将这些内存块交给对应的线程使用。
  • 当我们需要访问这些映射关系时,是当这些内存从centralcache返还给pagecache时,这时我们可能需要将前后页的内存合并,那么此时必然不会有其他的线程需要去分配这片内存的映射关系,因为这块内存还不能被使用。

所以我们利用基数树来保存内存块的映射关系时,可以不需要对pagecache加这把大锁。
而如果加锁的话会导致上下文的切换等等额外开销,所以使用基数树可以很大的提升我们在高并发情况下的效率。


template<int BITS>
class TCMalloc_PageMap1 {
private:
	static const int LENGTH = 1 << BITS;
	size_t* _array;

public:
	typedef uintptr_t Number;

	TCMalloc_PageMap1() {
		_array = reinterpret_cast<size_t*>(malloc(sizeof(size_t) << BITS));
		memset(_array, 0, sizeof(size_t) << BITS);
	}

	bool ensure(Number x, size_t n) {
		return n <= LENGTH - x;
	}

	void PreallocateMoreMemory(){}

	size_t get(Number k)const {
		if ((k >> BITS) > 0)
			return 0;
		return _array[k];
	}
	void set(Number k, size_t v) {
		_array[k] = v;
	}

	size_t Next(Number k)const {
		while (k < (1 << BITS)) {
			if (_array[k] != nullptr)
				return _array[k];
			k ++ ;
		}
		return nullptr;
	}
};


template<int BITS>
class TCMalloc_PageMap2
{
private:
	static const int ROOT_BITS = 5;
	static const int ROOT_LENGTH = 1 << ROOT_BITS;   //32
	static const int LEAF_BITS = BITS - ROOT_BITS;
	static const int LEAF_LENGTH = 1 << LEAF_BITS;  // 2^15

	//leaf node
	struct Leaf {

		Span* values[LEAF_LENGTH];

	};

	Leaf* _root[ROOT_LENGTH];


public:
	typedef size_t Number;
	
	explicit TCMalloc_PageMap2() {
		memset(_root, 0, sizeof(_root));
		PreallocateMoreMemory();
	}

	/// <summary>
	/// 预分配内存
	/// </summary>
	void PreallocateMoreMemory() {
		Ensure(0, 1 << BITS);
	}

	/// <summary>
	/// 开基数树所需全部内存
	/// </summary>
	/// <param name="start"></param>
	/// <param name="n"></param>
	/// <returns></returns>
	bool Ensure(Number start, size_t n) {
		for (Number key = start; key <= start + n - 1;) {
			const Number i1 = key >> LEAF_BITS;

			if (i1 >= ROOT_LENGTH)
				return false;

			if (_root[i1] == nullptr) {
				Leaf* leaf = new Leaf;
				if (leaf == nullptr)
					return false;
				memset(leaf, 0, sizeof(leaf));
				_root[i1] = leaf;
			}

			key = ((key >> LEAF_BITS) + 1) << LEAF_BITS;
		}
		return true;
	}
	
	Span* get(Number k)const {
		const Number i1 = k >> LEAF_BITS;
		const Number i2 = k & (LEAF_LENGTH - 1);
		if ((k >> BITS) > 0 || _root[i1] == nullptr) {
			return nullptr;
		}
		return _root[i1]->values[i2];
	}

	void set(Number k, Span* v) {
		const Number i1 = k >> LEAF_BITS;
		const Number i2 = k & (LEAF_LENGTH - 1);
		assert(i1 < ROOT_LENGTH);
		_root[i1]->values[i2] = v;
	}


	Span*& operator[](Number k) {
		const Number i1 = k >> LEAF_BITS;
		const Number i2 = k & (LEAF_LENGTH - 1);
		assert(i1 < ROOT_LENGTH);
		return _root[i1]->values[i2];
	}

	void erase(Number k) {
		const Number i1 = k >> LEAF_BITS;
		const Number i2 = k & (LEAF_LENGTH - 1);
		assert(i1 < ROOT_LENGTH);
		_root[i1]->values[i2] = nullptr;
	}

	void* Next(Number k) const {
		while (k < (1 << BITS)) {
			const Number i1 = k >> LEAF_BITS;
			Leaf* leaf = _root[i1];
			if (leaf != nullptr) {
				for (Number i2 = k & (LEAF_LENGTH - 1); i2 < LEAF_LENGTH; i2++) {
					if (leaf->values[i2] != nullptr) {
						return leaf->values[i2];
					}
				}
			}
			k = (i1 + 1) << LEAF_BITS;
		}
		return nullptr;
	}
};
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值