如何高效率的申请“公租房”—-高并发内存池

高并发内存池

  • 项目介绍
  • 何谓内存池
    • 池化技术
    • 内存池
    • 内存池主要解决的问题
      • 内存碎片
    • malloc
      • malloc()和free()的分配算法
  • 开胃菜—-设计一个定长的内存池
  • 高并发内存池整体框架设计
  • thread cache
    • threadcache整体设计
    • 管理切分好的对象的自由链表
    • 对象大小的对齐映射规则
    • 映射的哪一个自由链表桶
    • threadcacheTLS无锁访问
  • central cache
    • central cache整体设计
    • central cache结构设计
      • 页号的类型
      • span的结构
      • 双链表结构
      • central cache的结构
    • central cache核心实现
      • central cache的实现方式
      • 慢开始反馈调节算法
      • 从中心缓存获取对象
      • 从中心缓存获取一定数量的对象
      • 插入一段范围的对象到自由链表
  • page cache
    • page cache整体设计
    • page cache的实现方式
    • pagecache中获取Span
      • 获取一个非空的span
      • 获取一个k页的span
  • 申请内存过程联调
    • ConcurrentAlloc函数
    • 申请内存过程联调测试一
    • 申请内存过程联调测试二
  • thread cache回收内存
  • central cache回收内存
  • page cache回收内存
  • 释放内存过程联调
    • ConcurrentFree函数
    • 释放内存过程联调测试
  • 大于256KB的大块内存申请问题
    • 申请
    • 释放
    • 测试
  • 使用定长内存池配合脱离使用new
  • 释放对象时优化为不传对象大小
    • 读取映射关系时的加锁问题
  • 多线程环境下对比malloc测试
    • 固定大小内存的申请和释放
    • 不同大小内存的申请和释放
  • 复杂问题的调试技巧
    • 条件断点
    • 查看函数栈帧
    • 疑似死循环时中断程序
  • 性能瓶颈分析
  • 针对性能瓶颈使用基数树进行优化
    • 单层基数树
    • 二层基数树
    • 三层基数树
  • 使用基数树进行优化代码实现
  • 项目源码

项目介绍

当前项目是实现一个高并发的内存池,他的原型是google的一个开源项目tcmalloc,tcmalloc全称Thread-Caching Malloc,即线程缓存的malloc,实现了高效的多线程内存管理,用于替代系统的内存分配相关的函数(malloc、free)。
我们这个项目是把tcmalloc最核心的框架简化后拿出来,模拟实现出一个自己的高并发内存池,目的就是学习tcamlloc的精华。

何谓内存池

池化技术

池化技术:程序先向系统申请过量的资源,然后自己管理,以备不时之需。

目的:之所以要申请过量的资源,是因为每次申请该资源都有较大的开销,不如提前申请好了,这样使用时就会变得非常快捷,大大提高程序运行效率。
在计算机中,有很多使用“池”这种技术的地方,除了内存池,还有连接池、线程池、对象池等。以服务器上的线程池为例,它的主要思想是:先启动若干数量的线程,让它们处于睡眠状态,当接收到客户端的请求时,唤醒池中某个睡眠的线程,让它来处理客户端的请求,当处理完这个请求,线程又进入睡眠状态。

内存池

  • 内存池是指程序预先从操作系统申请一块足够大内存,此后,当程序中需要申请内存的时候,不是直接向操作系统申请,而是直接从内存池中获取;同理,当程序释放内存的时候,并不真正将内存返回给操作系统,而是返回内存池。当程序退出(或者特定时间)时,内存池才将之前申请的内存真正释放。

内存池主要解决的问题

  1. 解决效率问题。
  2. 站在内存分配器的角度,解决内存碎片的问题。

内存碎片

内存碎片分为外碎片和内碎片。

  1. 外部碎片是一些空闲的连续内存区域太小,这些内存空间不连续,以至于合计的内存足够,但是不能满足一些的内存分配申请需求。
  2. 内部碎片是由于一些对齐的需求,导致分配出去的空间中一些内存无法被利用。
    如何高效率的申请“公租房”----高并发内存池

    malloc() 相当于向操作系统“批发”了一块较大的内存空间,然后“零售”给程序用。当全部“售完”或程序有大量的内存需求时,再根据实际需求向操作系统“进货”。当然 malloc() 在向程序零售堆空间时,必须管理它批发来的堆空间,不能把同一块地址出售两次,导致地址的冲突。于是 malloc() 需要一个算法来管理堆空间,这个算法就是堆的分配算法。

    malloc()和free()的分配算法

    在程序运行过程中,堆内存从低地址向高地址连续分配,随着内存的释放,会出现不连续的空闲区域,如下图所示:

    如何高效率的申请“公租房”----高并发内存池
    next 是指针,指向下一个内存块,used 用来表示当前内存块是否已被使用。这样,整个堆区就会形成如下图所示的链表:
    如何高效率的申请“公租房”----高并发内存池
    仍然以上图为例,当程序释放掉第三个内存块时,就会形成新的空闲区域,free() 会将第二、三、四个连续的空闲区域合并为一个,如下图所示:
    如何高效率的申请“公租房”----高并发内存池
    链表是一种经典的堆内存管理方式,经常被用在教学中,很多C语言教程都会提到“栈内存的分配类似于数据结构中的栈,而堆内存的分配却类似于数据结构中的链表”就是源于此。

    链表式内存管理虽然思路简单,容易理解,但存在很多问题,例如:

    • 一旦链表中的 pre 或 next 指针被破坏,整个堆就无法工作,而这些数据恰恰很容易被越界读写所接触到。
    • 小的空闲区域往往不容易再次分配,形成很多内存碎片。
    • 经常分配和释放内存会造成链表过长,增加遍历的时间。

    针对链表的缺点,后来人们提出了位图和对象池的管理方式,而现在的 malloc() 往往采用多种方式复合而成,不同大小的内存块往往采用不同的措施,以保证内存分配的安全和效率。
    不管具体的分配算法是怎样的,为了减少系统调用,减少物理内存碎片,malloc() 的整体思想是先向操作系统申请一块大小适当的内存,然后自己管理,这就是内存池(Memory Pool)。

    开胃菜—-设计一个定长的内存池

    malloc其实就是一个通用的内存池,在什么场景下都可以使用,但这也意味着malloc在什么场景下都不会有很高的性能,因为malloc并不是针对某种场景专门设计的。

    如何高效率的申请“公租房”----高并发内存池

    如何实现定长/p>

    可以使用非类型模板参数,使得在该内存池中申请到的对象的大小都是N。

    此外,定长内存池也叫做对象池,在创建对象池时,对象池可以根据传入的对象类型的大小来实现“定长”,因此我们可以通过使用模板参数来实现“定长”,比如创建定长内存池时传入的对象类型是int,那么该内存池就只支持4字节大小内存的申请和释放。

    定长内存池中应该包含哪些成员变量/p>

    对于向堆申请到的大块内存,我们可以用一个指针来对其进行管理,但仅用一个指针肯定是不够的,我们还需要用一个变量来记录这块内存的长度。

    如何高效率的申请“公租房”----高并发内存池

    内存池如何为我们申请对象/p>

    当我们申请对象时,内存池应该优先把还回来的内存块对象再次重复利用,因此如果自由链表当中有内存块的话,就直接从自由链表头删一个内存块进行返回即可。

    如何高效率的申请“公租房”----高并发内存池
    注意:
    1. 由于当内存块释放时我们需要将内存块链接到自由链表当中,因此我们必须保证切出来的对象至少能够存储得下一个地址,所以当对象的大小小于当前所在平台指针的大小时,需要按指针的大小进行内存块的切分。
    2. 当大块内存已经不足以切分出一个对象时,我们就应该调用我们封装的SystemAlloc函数,再次向堆申请一块内存空间,此时也要注意及时更新_memory指针的指向,以及_remainBytes的值。

    注意:与释放对象时需要显示调用该对象的析构函数一样,当内存块切分出来后,我们也应该使用定位new,显示调用该对象的构造函数对其进行初始化。

    内存池如何管理释放的对象/p>

    对于还回来的定长内存块,我们可以用自由链表将其链接起来,向自由链表插入被释放的内存块时,先让该内存块的前4个字节或8个字节存储自由链表中第一个内存块的地址,然后再让指向该内存块即可,也就是一个简单的链表头插操作。

    如何高效率的申请“公租房”----高并发内存池

    如何让一个指针在32位平台下解引用后能向后访问4个字节,在64位平台下解引用后能向后访问8个字节/p>

    首先我们得知道,32位平台下指针的大小是4个字节,64位平台下指针的大小是8个字节。而指针指向数据的类型,决定了指针解引用后能向后访问的空间大小,当我们需要访问一个内存块的前4/8个字节时,我们就可以先该内存块的地址先强转为二级指针,由于二级指针存储的是一级指针的地址,二级指针解引用能向后访问一个指针的大小,因此在32位平台下访问的就是4个字节,在64位平台下访问的就是8个字节,此时我们访问到了该内存块的前4/8个字节。
    注意:在释放对象时,我们应该显示调用该对象的析构函数清理该对象,因为该对象可能还管理着其他某些资源,如果不对其进行清理那么这些资源将无法被释放,就会导致内存泄漏。

    定长内存池整体代码如下:

    来源:锤子哥学编程

    声明:本站部分文章及图片转载于互联网,内容版权归原作者所有,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2022年7月13日
下一篇 2022年7月13日

相关推荐