项目篇:高并发内存池

基础概念

池化技术

  • 所谓池化技术,就是程序先向系统申请过量的资源,然后自己管理,当程序中需要申请内存时,不是直接向操作系统申请,而是直接从内存池中获取,释放内存时也不是将内存返回给操作系统,而是返回内存池中。
  • 因为每次申请该资源都有较大的开销,这样提前申请好了,使用时就会非常快捷,能够大大提高程序运行效率。
  • 在计算机中有很多使用这种池技术的地方,例如线程池、连接池等。

动态内存申请malloc

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

定长内存池设计

设计思路

  • 开辟内存:

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

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

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

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

三层设计思路

  • 第一层: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对象,并且合并相邻的页,组成更大的页,缓解内存碎片的问题。

image.png

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

  • 线程申请内存:

    • 线程申请内存时,会有不同规格的内存申请(4字节、5字节等),根据范围划定不同的自由链表,设计多个自由链表管理不同规格的内存小块。
    • 实质就相当于使用多个定长内存池的自由链表
    • 每个内存小块采用向上对齐原则(可能会出现多申请内存的情况,这就是内碎片)
    • 例如:

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

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

    image.png

  • 注意:_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又是一个单向自由链表。

    image.png

  • 内存申请

    • 当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需要加整体锁(因为是所有线程共享的)
    image.png
  • 申请内存:

    • 当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...

15 声望
0 粉丝
0 条评论
推荐阅读
排序算法
源码地址(gitee)堆排序源码地址:[链接]其他排序方法源码地址:[链接]常见排序算法插入排序:直接插入排序希尔排序选择排序:直接选择排序堆排序交换排序:冒泡排序快速排序归并排序直接插入排序核心思想:将一...

夜枫微凉阅读 365

麒麟操作系统 (kylinos) 从入门到精通 - 研发环境 - 第二十一篇 C++/C语言开发环境搭建
类别:笔记本型号:中国长城 NF14C硬件平台:飞腾处理器(ArmV8 指令集)系统:银河麒麟操作系统 V10 SP1(2203) 关键词:信创,麒麟系统,linux,c++,c,内核飞腾,arm

码上世界1阅读 2.3k评论 1

封面图
万字避坑指南!C++的缺陷与思考(下)
导读 | 在万字避坑指南!C++的缺陷与思考(上)一文中,微信后台开发工程师胡博豪,分享了C++的发展历史、右值引用与移动语义、类型说明符等内容,深受广大开发者喜爱!此篇,我们邀请作者继续总结其在C++开发过...

腾讯云开发者4阅读 464评论 1

一种将函数模板定义和声明分开的方法
  在 C++ 中为了操作简洁引入了函数模板。所谓的函数模板实际上是建立一个通用函数,其函数类型或形参类型不具体指定,用一个虚拟的类型来表达,这个通用函数就称为函数模板。

Sharemaker阅读 838

封面图
Workflow的JSON解析器
Workflow中有一个小而美的json-parser,一千行代码写得非常典雅精致。不仅可以学习到经典的C语言写法、递归解析的架构、与内核近似的编码风格、简洁的接口设计,而且也非常方便引入项目中作为轻量级的json解析器...

1412阅读 774

C发展史的特点与常见的C语言程序
1、1963年,剑桥人学将ALGOL 60语言发展成为CPL语言。2、1967年,朝侨大学的Martin Richards 对CPL语言进行了简化,产生了BCPL语言。3、1970年,美国贝尔实验室的Ken Thompson将BCPL中的精华提炼出来,并为它起了...

菜鸟明轩阅读 710

零钱兑换
{代码...}

阿芯爱编程阅读 676

封面图
15 声望
0 粉丝
宣传栏