根底概念

池化技术

  • 所谓池化技术,就是程序先向零碎申请适量的资源,而后本人治理,当程序中须要申请内存时,不是间接向操作系统申请,而是间接从内存池中获取,开释内存时也不是将内存返回给操作系统,而是返回内存池中。
  • 因为每次申请该资源都有较大的开销,这样提前申请好了,应用时就会十分快捷,可能大大提高程序运行效率。
  • 在计算机中有很多应用这种池技术的中央,例如线程池、连接池等。

动态内存申请malloc

  • C++中动静申请内存都是通过malloc去申请的,但实际上咱们并不是间接去堆中获取内存的,而malloc就是一个内存池。
  • malloc() 相当于向零碎 “零售” 了一块较大的内存空间,而后“批发” 给程序应用,当全副应用完或者程序有大量内存需要时,再依据需要向操作系统申请内存。

定长内存池设计

设计思路

  • 开拓内存:

    • 应用malloc开拓一大块内存,让_memory指针指向这个大块内存
    • _memory 设置为char* 类型,是为了不便切割时_memory向后挪动多少字节数。
  • 申请内存:

    • 将_memory强转为对应类型,而后赋值给对方,_memory指针向后挪动对应字节数即可。
    • 如果有存在曾经切割好的小块内存,则优先应用小块内存。
  • 开释内存:

    • 用类型链表的构造来进行存储。
    • 用以后小块内存的头4字节存储下一个小块内存的地址,最初用_freeList指针指向第一个小块内存的地址(并不是将内存开释给操作系统)
    • 所以开拓内存时,开拓的内存大小必须大于或等于一个指针类型的大小。
  • 代码地位:高并发内存池我的项目中的ObjectPool.h 文件。

高并发内存池整体设计框架

三层设计思路

  • 第一层:thread cache(线程缓存):

    • 每个线程独享一个thread cache,用于小于256k的内存分配情况,线程从这里申请内存不须要加锁(因为其余线程无法访问以后线程的 thread cache,没有竞争关系)
  • 第二层:central cache(核心缓存):

    • 所有线程共享一个central cache,thread cache 是按需从central cache中获取对象,central cache在适合的时候会发出thread cache中的对象,防止一个线程占用太多资源。
    • central cache 是所有线程共享的,所以存在竞争关系,须要加锁;这里应用的锁是桶锁,并且因为只有thread cache没有内存时才会申请内存,所以这里竞争不会太强烈。
  • 第三层:page cache(页缓存):

    • 页缓存存储的内存是以页为单位进行存储的及调配的。central cache没有内存时,则会从page cache中申请肯定数量的page,并切割成定长大小的小内存块。
    • 当一个span的几个跨度页的对象都回收后,page cache会回收central cache满足条件的span对象,并且合并相邻的页,组成更大的页,缓解内存碎片的问题。

thread cache 设计思路(线程缓存)

  • 线程申请内存:

    • 线程申请内存时,会有不同规格的内存申请(4字节、5字节等),依据范畴划定不同的自在链表,设计多个自在链表治理不同规格的内存小块。
    • 本质就相当于应用多个定长内存池的自在链表
    • 每个内存小块采纳向上对齐准则(可能会呈现多申请内存的状况,这就是内碎片)
    • 例如:

      • 须要9字节,则开拓一个大小为2个8字节的空间的节点
      • 须要100字节,则开拓一个大小为13个8字节的空间的节点。
  • 线程对齐规定:

    • 整体管制在最多10%左右的内碎片节约
    • 总计设计208个桶
    • [0,15]个桶,每个桶的对齐数相差8字节(最高128字节对齐)
    • [16,71]个桶,每个桶的对齐数相差16字节(最高1024字节对齐)
    • 以此类推

  • 留神:_freeLists是一个数组,每个元素都是自在链表类型(即存储自在链表的头结点)
  • 线程开释内存

    • 开释内存后:采纳自在链表的构造来治理切好的小块内存(每一个切分好的小块内存就是一个节点)
    • 具体方法是:用切分好的小块内存的前4字节或8字节来存储下一个小块内存的地址。
    • 插入节点时,采纳头插的形式。

线程缓存无锁设计(TLS)

  • TLS:thread local storage 线程本地存储(linux和Windows下有各自的TLS)
  • TLS是一种变量的存储形式,这个变量在它所在的线程内是全局可拜访的,然而不能被其余线程拜访,这样就保障了线程的独立性。
  • 动态TLS应用办法:

    • _declspec(thread) DWORD data=0;
    • 申明了一个 _declspec(thread) 类型的变量,会为每一个线程创立一个独自的拷贝。
  • 原理:

    • 在x86 CPU上,将为每次援用的动态TLS变量生成3个辅助机器指令
    • 如果在过程中创立子线程,那么零碎将会捕捉它并且主动调配一另一个内存块,以便寄存新线程的动态TLS变量。
    //每个线程刚创立时,就会取得这个指针,每个线程的指针是不同的。static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;//static 润饰,能够保障变量只在以后文件可见。

central cache设计思路(核心缓存)

  • central cache也是一个哈希桶构造,每个哈希桶地位挂载的是SpanList自在链表构造。
  • Span治理的是以页为单位的大块内存(一页为8kb(32位零碎下))
  • 每个Span中的大内存依据映射关系被切成了一个个的小块内存对象,而后挂载在Span上。
  • 因为核心缓存是所有线程共享的,只须要定义一个对象,所以这里须要将 central cache 设计为单例模式(这里采纳饿汉模式的设计办法)
  • 留神:

    • span是双向链表,而span下挂载的小块内存对象是单链表。
    • 核心缓存须要加桶锁。
    • _spanLists 是一个数组,数组中每个元素都是一个span自在链表的_head头指针。
    • 每个span又是一个单向自在链表。

  • 内存申请

    • 当thread cache中没有内存时,就会批量向central cache申请一些内存对象,从对应的span中取出小块内存对象给thread cache,这个过程是须要加锁的(加桶锁)
    • 这里批量获取对象的数量应用了相似网络tcp协定拥塞管制的慢开始算法。
    • 如果所有的span都为空了(即central cache应用完了),则将空的span链在一起,向page cache申请一个span对象,span对象中是一些以页为单位的内存,须要切成对应的小块内存对象,并链接起来,挂到span中。
    • central cache中每一个span都有一个use_count,调配一个对象给thread cache,就加加。
  • 内存开释:

    • 当thread_cache将内存开释回central cache中的时,开释回来就减减use_count。
    • 当use_count减到0时则示意所有对象都回到了span,则将span开释回page cache,page cache中会对前后相邻的闲暇页进行合并。

page cache设计思路(页缓存)

  • page cache中也是哈希桶构造,然而每个节点存储的都是span
  • 因为页缓存是所有线程共享的,只须要定义一个对象,所以这里将 page cache 设计为单例模式(这里采纳饿汉模式的设计办法)
  • 留神:page cache须要加整体锁(因为是所有线程共享的)
  • 申请内存:

    • 当central cache向page cache申请内存时,page cache先查看对应地位有没有span,如果没有则向更大页寻找一个span,如果找到则决裂成两个。
    • 例如:申请的是4page,4page前面没有挂span,则向前面寻找更大的span,假如在10page地位找到一个span,则将10page span决裂为一个4page span和一个6page span,把4page的span返回给central cache,把6page的span挂载到对应的6号桶中去。
    • 如果找到128 page都没有适合的span,则向零碎应用mmap、brk或者是VirtualAlloc等形式申请一个128page 的span挂在自在链表中,再反复1中的过程。
    • 第一次申请内存时,page cache是空的,通过下面的过程后会向堆申请一个128page的span,而后反复下面的过程,将128page的span进行决裂和从新挂载。
  • 开释内存:

    • 如果central cache开释回一个span,则顺次寻找span的前后page id的span,看是否能够合并,如果合并持续向前寻找。这样就能够将切小的内存合并膨胀成大的span,缩小内存碎片。

源码地址

https://gitee.com/BJFyfwl/Lin...