高并发内存池

Posted _BitterSweet

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了高并发内存池相关的知识,希望对你有一定的参考价值。

1.什么是内存池

1.1 池化技术

  • 池是在计算机技术中经常使用的一种设计模式,其内涵在于:将程序中需要经常使用的核心资源
    先申请出来,放到一个池内,由程序自己管理,这样可以提高资源的使用效率,也可以保证本程
    序占有的资源数量。 经常使用的池技术包括内存池、线程池和连接池等,其中尤以内存池和线程
    池使用最多

1.2 内存池

  • 内存池(Memory Pool) 是一种动态内存分配与管理技术。我们习惯使用new,delete,malloc,free等API申请释放内存,这样导致的后果就是:当程序长时间运行时,由于所申请内存块的大小不定,频繁使用时会造成大量的内存碎片从而降低程序和操作系统的性能。
  • 内存池则是提前向系统一块较大的内存留作备用,当我们申请内存时,从池中取出一块动态分配,当我们释放内存时,把释放的内存再放入内存池中,再次申请就再次拿出来使用,并尽量与周边的空闲内存块合并。若内存池不够时,则自动扩大内存池,从操作系统中申请更大的内存

2.为什么需要内存池

  • 解决内存碎片问题
  • 提高申请效率

3.高并发内存池

  • 现在很多开发环境都是多核多线程的,在申请内存的场景下,一定存在激烈的锁竞争问题,所以需要考虑如下的问题

1.内存碎片问题怎么解决?
2.性能问题
3.多核多线程环境下,锁的竞争问题

  • 高并发内存池主要由三个部分组成
    在这里插入图片描述

  • threadcache:线程缓存是每一个线程独有的,用小于64KB的内存分配,线程从这里申请是不需要加锁的,每一个线程独享一个cache(TLS)技术,高效并发

  • centralcache:中心缓存是所有线程共享的,threadcache按照需要从centralcache中获取内存,centralcache再周期性回收threadcache中的对象,避免一个线程占用过多的内存,导致其他线程内存紧张,所以在这层缓存中是存在锁竞争的问题,所以这里为了均衡资源则需要加锁

  • pagecache:页缓存是当中心缓存不够时,给中心缓存分配出一定数量的页,将这些页切割成大小相等的内存块分配给span

  • 当thread cache中的内存归还给central cache的时候,如果central cache中的span满足归还条件的话,就将这个span归还给page cache,然后page cache在将归还回来的内存合并成更大的页

3.1 threadcache

  • 首先通过TLS技术保存每个线程本地的threadcache指针,这样在大部分情况下当线程缓存在申请释放内存时不需要加锁,因为每一个线程都拥有了自己唯一的全局变量
// thread cache本质是由一个哈希映射的对象自由链表构成
class ThreadCache
{
public:
 // 申请和释放内存对象
 void* Allocate(size_t size);
 void Deallocate(void* ptr);
 // 从中心缓存获取对象
 void* FetchFromCentralCache(size_t index, size_t size);
 // 释放对象时,链表过长时,回收内存回到中心堆
 void ListTooLong(FreeList* list, size_t size);
private:
 FreeList _freeList[NLISTS];   // 自由链表
};

static __declspec(thread) ThreadCache* tls_threadcache = nullptr;

在这里插入图片描述
申请内存

  • 当申请的内存小于等于64K的话,直接在threadcache中申请,计算size在自由链表中的位置,如果自由链表中有内存对象时,直接从FistList[i]中Pop一下对象,时间复杂度是O(1),且没有锁竞争
  • 当FreeList[i]中没有对象时,则批量从central cache中获取一定数量的对象,插入到自由链表并返回一个对象

释放内存

  • 当释放内存小于64K时,直接释放回threadcache中,计算size在自由链表中的位置,将对象Push到FreeList[i]
  • 如果链表长度过长,则回收一部分内存对象到centralcache中

3.2 centralcache

  • central cache本质是由一个哈希映射的span对象自由链表构成
  • 为了保证全局只有唯一的central cache,这个类被设计成了单例模式
  • 如果thread cache未用内存过多就会导致资源分配不均衡,线程大量申请内存后,thread cache就会大量空闲,当其他线程需要使用资源时,就没有那么多的内存可以使用了,导致其他线程存在线程饥饿问题,所以为了解决这样的问题,设计一个中心缓存用来均衡资源
//设计成单例模式
class CentralCache
{
public:
	static CentralCache* Getinstence()
	{
		return &_inst;
	}

	//从page cache获取一个span
	Span* GetOneSpan(SpanList& spanlist, size_t byte_size);

	//从中心缓存获取一定数量的对象给threa cache
	size_t FetchRangeObj(void*& start, void*& end, size_t n, size_t byte_size);

	//将一定数量的对象释放给span跨度
	void ReleaseListToSpans(void* start, size_t size);

private:
	SpanList _spanlist[NLISTS];

private:
	CentralCache(){}//声明不实现,防止默认构造,自己创建

	CentralCache(CentralCache&) = delete;
	static CentralCache _inst;
};

在这里插入图片描述

3.3 pagecache

在这里插入图片描述

class PageCache
{
public:
	static PageCache* GetInstence()
	{
		return &_inst;
	}

	Span* AllocBigPageObj(size_t size);
	void FreeBigPageObj(void* ptr, Span* span);

	Span* _NewSpan(size_t n);
	Span* NewSpan(size_t n);//获取的是以页为单位

	//获取从对象到span的映射
	Span* MapObjectToSpan(void* obj);

	//释放空间span回到PageCache,并合并相邻的span
	void ReleaseSpanToPageCache(Span* span);

private:
	SpanList _spanlist[NPAGES];
	//std::map<PageID, Span*> _idspanmap;
	std::unordered_map<PageID, Span*> _idspanmap;

	std::mutex _mutex;
private:
	PageCache(){}

	PageCache(const PageCache&) = delete;
	static PageCache _inst;
};

4.项目优点与缺陷

优点

  1. 高并发:高并发是因为对于每一个线程都有自己的一个线程缓存,当每一个线程申请内存的时候就不需要每次要到系统申请内存直接到自己的线程缓存上申请内存就好了。就不会牵扯到多个线程访问同一份资源,就达到了高并发的目的。
  2. 提高效率:每一次使用内存的时候,提前将内存都已经分配好了,直接用内存就不需要再次从系统申请内存了。也就是减少了调用系统调用函数的次数,从而提高了效率。并且有着中心缓存还进行多个线程之前的均衡,不会让一个线程占用着许多个内存不使用,导致其他的线程想要申请内存的时候申请不到内存的情况。当一个线程内部的内存块大于一个水位线的时候,就将内存全都释放到中心缓存中。
  3. 缓解内存碎片问题:该项目将内存碎片大约控制在12%左右,因为对于线程缓存不使用的内存就会归还到中心缓存的一个span上,而中心缓存的span上的内存只要没有线程使用的话,就将这个span再次归还到页缓存上,归还到页缓存的时候就会对多个span进行合并,从而将小的内存合并成大的内存。

缺点

  1. 项目独立性不足,项目中并没有完全脱离malloc,比如在spanlist结构中,我们有new的操作,new的底层还是malloc,所以并没有完全脱离malloc
  2. 解决方案就是增加一个对象池,对象池的内存直接用brk,virtualloc等技术向OS申请,new span替换成对象池申请,这样就可以达到完全脱离malloc

以上是关于高并发内存池的主要内容,如果未能解决你的问题,请参考以下文章

C++实现的高并发内存池

C++实现的高并发内存池

高并发内存池

高并发内存池

高并发内存池

高并发内存池