关于c++:项目篇高并发内存池

4次阅读

共计 3469 个字符,预计需要花费 9 分钟才能阅读完成。

根底概念

池化技术

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

动态内存申请 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…

正文完
 0