C/C++|项目实践—高并发内存池


文章目录

  • 1. 内存池简介
  • 2. 为什么需要内存池?
    • 2.1 内存碎片问题
    • 2.2 申请效率问题
  • 3. 高并发内存池
    • 3.1 第一层: thread cache
    • 3.2 第二层: Central Cache
    • 3.3 第三层: Page Cache
  • 4. 高并发内存池优点
  • 5. 扩展知识
  • 6. 源码链接

1. 内存池简介
  • 池化技术:将程序中需要经常使用的核心资源先申请出来,放到一个池内,由程序自己管理,这样可以提高资源的使用效率,也可以保证本程序占有的资源数量。
  • 内存池(Memory Pool) 是一种动态内存分配与管理技术。 通常情况下,程序员习惯直接使用 new、delete、malloc、free 等API申请分配和释放内存,这样导致的后果是:当程序长时间运行时,由于所申请内存块的大小不定,频繁使用时会造成大量的内存碎片从而降低程序和操作系统的性能。内存池则是在真正使用内存之前,先申请分配一大块内存(内存池)留作备用,当程序员申请内存时,从池中取出一块动态分配,当程序员释放内存时,将释放的内存再放入池内,再次申请池可以 再取出来使用,并尽量与周边的空闲内存块合并。若内存池不够时,则自动扩大内存池,从操作系统中申请更大的内存池。
2. 为什么需要内存池? 2.1 内存碎片问题
  • 假设系统依次分配了16byte、8byte、16byte、4byte,还剩余8byte未分配。这时要分配一个24byte的空间,操作系统回收了一个上面的两个16byte,总的剩余空间有40byte,但是却不能分配出一个连续24byte的空间,这就是内存碎片问题。
    C/C++|项目实践—高并发内存池
    文章图片
2.2 申请效率问题
  • 先将经常所需要的内存进行池化,预先保存,需要内存的时候直接从池中拿取,大大提高内存效率。避免频繁调用函数。
3. 高并发内存池 现代很多的开发环境都是多核多线程,在申请内存的场景下,必然存在激烈的锁竞争问题。所以实现的内存池需要考虑以下几方面的问题。
  1. 内存碎片问题
  2. 性能问题
  3. 多核多线程环境下,锁竞争问题
高并发内存池(concurrent memory pool)主要由以下3个部分构成:
  1. thread cache线程缓存是每个线程独有的,用于小于64k的内存的分配,线程从这里申请内存不需要加锁,每个线程独享一个cache,这也就是这个并发线程池高效的地方。
  2. central cache中心缓存是所有线程所共享,thread cache是按需从central cache中获取的对象。central cache周期性的回收thread cache中的对象,避免一个线程占用了太多的内存,而其他线程的内存吃紧达到内存分配在多个线程中更均衡的按需调度的目的
    central cache是存在竞争的,所以从这里取内存对象是需要加锁桶锁),不过一般情况下在这里取内存对象的效率非常高,所以这里竞争不会很激烈。
  3. page cache:页缓存是在central cache缓存上面的一层缓存,存储的内存是以页为单位存储及分配的,central cache没有内存对象时,从page cache分配出一定数量的page,并切割成定长大小的小块内存,分配给central cache。page cache会回收central cache满足条件的span对象,并且合并相邻的页,组成更大的页,缓解内存碎片的问题C/C++|项目实践—高并发内存池
    文章图片
3.1 第一层: thread cache 怎么实现每个线程都拥有自己唯一的线程缓存?
  • 为了避免加锁带来的效率,在Thread Cache中**使用thread local storageTLS**保存每个线程本地的ThreadCache的指针,这样Thread Cache在申请释放内存时是不需要锁的,因为每一个线程都拥有了自己唯一的全局变量
class ThreadCache { public: void* Allocate(size_t size); //分配内存 void Deallocate(void* ptr, size_t size); //释放内存 //从中心缓存中获取内存对象 void* FetchFromCentralCache(size_t index, size_t size); //当自由链表中的对象超过一次分配给threadcache的数量,则开始回收 void ListTooLong(FreeList* freelist, size_t byte); private: FreeList _freelist[NLISTS]; // 创建了一个自由链表数组 };

【C/C++|项目实践—高并发内存池】原理图
C/C++|项目实践—高并发内存池
文章图片

3.2 第二层: Central Cache
  • 中心缓存要实现为单例模式,保证全局只有一份实例。
  • 如果thread cache未用内存过多导致了另一个问题,就是内存资源分配不均衡的问题。当一个线程大量的开辟内存再释放的时候,这个线程中的thread cache势必会储存着大量的空闲内存资源,而这些资源是无法被其他线程所使用的,当其他的线程需要使用内存资源时,就可能没有更多的内存资源可以使用,这也就导致了其它线程的饥饿问题。为了解决这个问题,就有了central cache的设计。
  • Central Cache本质是由一个哈希映射的Span对象自由双向链表构成。一个span对象大小是恒定的4K大小,中心缓存数组每个元素指定了单个span划分成内存块的大小 (比如第一个8bytes 第二个16bytes等等),故他们能挂载的内存块数不一样
//设计成单例模式 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; };

原理图
C/C++|项目实践—高并发内存池
文章图片

3.3 第三层: Page Cache
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 _idspanmap; std::unordered_map _idspanmap; std::mutex _mutex; private: PageCache(){} PageCache(const PageCache&) = delete; static PageCache _inst; };

原理图
C/C++|项目实践—高并发内存池
文章图片

4. 高并发内存池优点
  • 高并发:
    高并发是因为对于每一个线程都有着自己的一个线程缓存,当每一个线程申请内存的时候就不需要每次要到系统申请内存直接到自己的线程缓存上申请内存就好了。就不会牵扯到多个线程访问同一份资源,就达到了高并发的目的,使用到了静态的TLS
  • 提高效率:
    每一次使用内存的时候,提前将内存都已经分配好了,直接用内存就不需要再次从系统申请内存了。也就是减少了调用系统调用函数的次数,从而提高了效率。并且有着中心缓存还进行多个线程之前的均衡,不会让一个线程占用着许多个内存不使用,导致其他的线程想要申请内存的时候申请不到内存的情况。当一个线程内部的内存块大于一个水位线的时候,就将内存全都释放到中心缓存中。
  • 解决了内存碎片:
    对于该项目将内存碎片控制在大约12%左右,关键就是对于外碎片进行了减少,因为对于线程缓存不使用的内存就会归还到中心缓存的一个span上,而中心缓存的span上的内存只要没有线程使用的话,就将这个span再次归还到页缓存上,归还到页缓存的时候就会对多个span进行合并,从而将小的内存合并成大的内存
5. 扩展知识 替换系统的malloc和free
  1. 对于不同的系统平台替换的方式是不同的。对于Linux下使用weak alias的方式进行实现的
  2. 对于其他的平台可以使用hook(钩子技术)替换系统的malloc和free:
项目的独立性不足
  • 不足:当前实现的项目中我们并没有完全脱离malloc,比如在内存池自身数据结构的管理中,如SpanList中的span等结构,我们还是使用的new Span这样的操作,new的底层使用的是malloc,所以还不足以替换malloc,因为们本身没有完全脱离它。
  • 解决方案:项目中增加一个定长的ObjectPool的对象池,对象池的内存直接使用brk、VirarulAlloc等向系统申请,new Span替换成对象池申请内存。这样就完全脱离的malloc,就可以替换掉malloc
6. 源码链接
  • 源码链接GitHub:https://github.com/2805294288/c_c-plus-plus_Code/tree/main/ConcurrentMemoryPool

    推荐阅读