C++项目 | 高并发内存池

  • 池化技术:线程池、内存池、连接池

内存池解决的问题:1、提高申请和释放内存的效率 2、解决内存碎片
内存碎片:频繁申请、释放小块内存,可能会导致内存碎片。分为两种场景:内碎片,外碎片(通常)

高并发内存池:对比malloc在多线程并发场景下申请内存的性能,减少锁竞争——让每个线程都有一个自己独立的内存池。

内存池需要考虑以下的问题:

  1. 内存碎片问题。
  2. 性能问题。
  3. 多核多线程环境下,锁竞争问题

一、高并发内存池的组成

C++项目 | 高并发内存池_第1张图片

threadCache(解决锁竞争)

就是一个哈希映射的内存桶(自由链表),threadCache(线程缓存)是每个线程独有的,用于小于64k的内存的分配,线程从这里申请内存不需要加锁,每个线程独享一个cache,这也就是这个并发线程池高效的地方。

申请内存:

  1. 当内存申请size<=64K时:在threadCache中申请内存,计算size在自由链表中的位置,如果自由链表中有内存对象时,直接从freeList[i]中Pop一下对象,时间复杂度是O(1),且没有锁竞
    争。
  2. 当freeList[i]中没有对象时,则批量从centralCache中获取一定数量的对象,插入到自由链表并返回一个对象。

释放内存:

  1. 当释放内存小于64K时:将内存释放回threadCache,计算size在自由链表中的位置,将对象Push到freeList[i].
  2. 当链表的长度过长,则回收一部分内存对象到central cache。

项目中用了静态TLS
为实现每个线程都拥有自己唯一的线程缓存,在threadCache中使用TLS(thread local storage)保存每个线程本地的threadCache的指针,这样threadCache在申请释放内存是不需要锁的,因为每一个线程都拥有了自己唯一的一个全局变量。
C++项目 | 高并发内存池_第2张图片

centralCache(居中调度均衡)

centralCache本质是由一个哈希映射的span对象自由双向链表构成,为了保证全局只有唯一的centralCache,这个类被可以设计成了单例饿汉模式,避免高并发下资源的竞争。

中心缓存是所有线程所共享,threadCache是按需从centralCache中获取的对象centralCache周期性的回收threadCache中的对象,避免一个线程占用了太多的内存,而其他线程的内存吃紧,达到内存分配在多个线程中更均衡的按需调度的目的。

申请内存:

  1. 当threadCache中没有内存时,就会批量向centralCache申请一些内存对象,centralCache
    也有一个哈希映射的freeList,freeList中挂着span,从span中取出对象给threadCache,这
    个过程是需要加锁的。
  2. centralCache中没有非空的span时,则将空的span链在一起,向pageCache申请一个span
    对象,span对象中是一些以页为单位的内存,切成需要的内存大小,并链接起来,挂到
    span中。
  3. centralCache的span中有一个usecount,每分配一个对象给threadCache,就++usecount。

释放内存:

  1. 当threadCache过长或者线程销毁,则会将内存释放回centralCache中,释放回来时–
    usecount。当usecount减到0时则表示所有对象都回到了span,则将span释放回pageCache,pageCache中会对前后相邻的空闲页进行合并。

C++项目 | 高并发内存池_第3张图片

pageCache(缓解内存碎片)

页缓存是在centralCache缓存上面的一层缓存,存储的内存是以为单位存
储及分配的,centralCache没有内存对象时,从pageCache分配出一定数量的page,并切
割成定长大小的小块内存,分配给centralCache。pageCache会回收centralCache满足条
件(usecount==0)的span对象,并且合并相邻的页,组成更大的页,缓解内存碎片的问题。

申请内存:

  1. 当centralCache向pageCache申请内存时,page Cache先检查对应位置有没有span,如果
    没有则向更大页寻找一个span,如果找到则分裂成两个。比如:申请的是4page,4page后面没有挂span,则向后面寻找更大的span,假设在10page位置找到一个span,则将
    10page位置挂的span分裂为一个4page span和一个6page span。
  2. 如果找到128 page都没有合适的span,则向系统使用mmap、brk或者是VirtualAlloc等方式
    申请128page span挂在自由链表中,再重复1中的过程。

释放内存:
3. 如果centralCache向pageCache释放回一个span,则依次寻找该span的前后page id的span,看是否可以合并,如果可以合并继续向前寻找,直到不能合并为止。这样就可以将切小的内存合并收缩成大的span,减少内存碎片。

C++项目 | 高并发内存池_第4张图片

二、细节理解

Span

struct Span
{
	PageID _pageId = 0;   // 页号
	size_t _n = 0;        // 页的数量

	Span* _next = nullptr; // 双向
	Span* _prev = nullptr;

	void* _list = nullptr;  // 大块内存切小链接起来,这样回收回来的内存也方便链接
	size_t _usecount = 0;	// 使用计数,==0 说明所有对象都回来了

	size_t _objsize = 0;	// 切出来的单个对象的大小
};

Span意为跨度,管理着centralCache和pageCache里面的以页为单位的内存对象,可以实现对自由链表中元素的管理,当pageCache中给到threadCache里面的对象都回来之后(usecount为0时),就可以把Span归还到pageCache,且页号的设置可以对相邻空闲span进行合并,缓解了内存碎片的问题。

  • 如何将threadCache中的内存对象还给它原来的span?

可以在pageCache中维护一个页号到Span的映射,当Span Cache给centralCache分配一个Span时,将这个映射更新到map中去,在Thread Cache还给Central Cache时,可以查这个std::map _idSpanMap找到对应的span。

向系统申请内存

  • Linux平台下使用brk或sbrk向系统直接申请堆内存
  • Windows平台下使用VirtualAlloc向系统申请堆内存
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
	void* ptr = VirtualAlloc(0, kpage*(1 << PAGE_SHIFT),
		MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
	// brk mmap等
#endif

	if (ptr == nullptr)
		throw std::bad_alloc();

	return ptr;
}

inline static void SystemFree(void* ptr)
{
#ifdef _WIN32
	VirtualFree(ptr, 0, MEM_RELEASE);
#else
	// sbrk unmmap等
#endif
}

三、扩展学习及项目实现的不足

  • 项目的独立性不足

当前实现的项目中我们并没有完全脱离malloc,比如SpanList中的span等结构,我们还是使用了new Span这样的操作,new的底层使用的是malloc,所以还不足以替换malloc,因为本身并没有完全脱离它。

解决方案:项目中增加一个定长的ObjectPool的对象池,对象池的内存直接使用brk、VirarulAlloc等向系统申请,new Span替换成对象池申请内存。这样就完全脱离malloc,替换掉malloc。

  • 平台及兼容性

1.Linux等系统下面,需要将VirtualAlloc替换为brk等。
2.x64系统下面,当前的实现支持不足。比如:id查找Span得到的映射,我们当前使用的是
map。在64位系统下面,这个数据结构在性能和内存等方面都是撑不住,需要改进后基数树。

中心思想:对于在该内存池中,从系统申请的内存就会一直存在于这个内存池中,不会再归还给系统,从系统申请内存的时候是按页进行申请的,对于归还内存的时候,只需要判断该内存在哪一个页中,直接归还给包含这个页的span。我们做的只是将申请来的内存进行标记从而来使用内存,逻辑上对内存池进行分配。
C++项目 | 高并发内存池_第5张图片

项目源码:https://github.com/LumosN/ConcurrentMemoryPool

你可能感兴趣的:(c++,内存管理,高并发)