- 池化技术:线程池、内存池、连接池
内存池解决的问题:1、提高申请和释放内存的效率 2、解决内存碎片
内存碎片:频繁申请、释放小块内存,可能会导致内存碎片。分为两种场景:内碎片,外碎片(通常)
高并发内存池:对比malloc在多线程并发场景下申请内存的性能,减少锁竞争——让每个线程都有一个自己独立的内存池。
内存池需要考虑以下的问题:
- 内存碎片问题。
- 性能问题。
- 多核多线程环境下,锁竞争问题
一、高并发内存池的组成
threadCache(解决锁竞争)
就是一个哈希映射的内存桶(自由链表),threadCache(线程缓存)是每个线程独有的,用于小于64k的内存的分配,线程从这里申请内存不需要加锁,每个线程独享一个cache,这也就是这个并发线程池高效的地方。
申请内存:
- 当内存申请size<=64K时:在threadCache中申请内存,计算size在自由链表中的位置,如果自由链表中有内存对象时,直接从freeList[i]中Pop一下对象,时间复杂度是O(1),且没有锁竞
争。 - 当freeList[i]中没有对象时,则批量从centralCache中获取一定数量的对象,插入到自由链表并返回一个对象。
释放内存:
- 当释放内存小于64K时:将内存释放回threadCache,计算size在自由链表中的位置,将对象Push到freeList[i].
- 当链表的长度过长,则回收一部分内存对象到central cache。
项目中用了静态TLS
为实现每个线程都拥有自己唯一的线程缓存,在threadCache中使用TLS(thread local storage)保存每个线程本地的threadCache的指针,这样threadCache在申请释放内存是不需要锁的,因为每一个线程都拥有了自己唯一的一个全局变量。
centralCache(居中调度均衡)
centralCache本质是由一个哈希映射的span对象自由双向链表构成,为了保证全局只有唯一的centralCache,这个类被可以设计成了单例饿汉模式,避免高并发下资源的竞争。
中心缓存是所有线程所共享,threadCache是按需从centralCache中获取的对象centralCache周期性的回收threadCache中的对象,避免一个线程占用了太多的内存,而其他线程的内存吃紧,达到内存分配在多个线程中更均衡的按需调度的目的。
申请内存:
- 当threadCache中没有内存时,就会批量向centralCache申请一些内存对象,centralCache
也有一个哈希映射的freeList,freeList中挂着span,从span中取出对象给threadCache,这
个过程是需要加锁的。 - centralCache中没有非空的span时,则将空的span链在一起,向pageCache申请一个span
对象,span对象中是一些以页为单位的内存,切成需要的内存大小,并链接起来,挂到
span中。 - centralCache的span中有一个usecount,每分配一个对象给threadCache,就++usecount。
释放内存:
- 当threadCache过长或者线程销毁,则会将内存释放回centralCache中,释放回来时–
usecount。当usecount减到0时则表示所有对象都回到了span,则将span释放回pageCache,pageCache中会对前后相邻的空闲页进行合并。
pageCache(缓解内存碎片)
页缓存是在centralCache缓存上面的一层缓存,存储的内存是以页为单位存
储及分配的,centralCache没有内存对象时,从pageCache分配出一定数量的page,并切
割成定长大小的小块内存,分配给centralCache。pageCache会回收centralCache满足条
件(usecount==0)的span对象,并且合并相邻的页,组成更大的页,缓解内存碎片的问题。
申请内存:
- 当centralCache向pageCache申请内存时,page Cache先检查对应位置有没有span,如果
没有则向更大页寻找一个span,如果找到则分裂成两个。比如:申请的是4page,4page后面没有挂span,则向后面寻找更大的span,假设在10page位置找到一个span,则将
10page位置挂的span分裂为一个4page span和一个6page span。 - 如果找到128 page都没有合适的span,则向系统使用mmap、brk或者是VirtualAlloc等方式
申请128page span挂在自由链表中,再重复1中的过程。
释放内存:
3. 如果centralCache向pageCache释放回一个span,则依次寻找该span的前后page id的span,看是否可以合并,如果可以合并继续向前寻找,直到不能合并为止。这样就可以将切小的内存合并收缩成大的span,减少内存碎片。
二、细节理解
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<PageID, Span*> _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<PageId, Span*>。在64位系统下面,这个数据结构在性能和内存等方面都是撑不住,需要改进后基数树。
中心思想:对于在该内存池中,从系统申请的内存就会一直存在于这个内存池中,不会再归还给系统,从系统申请内存的时候是按页进行申请的,对于归还内存的时候,只需要判断该内存在哪一个页中,直接归还给包含这个页的span。我们做的只是将申请来的内存进行标记从而来使用内存,逻辑上对内存池进行分配。
项目源码:https://github.com/LumosN/ConcurrentMemoryPool