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