C++项目之高并发内存池的主要部分实现(Thread-Cacheing malloc)

项目背景

本项目是对谷歌tcmalloc开源项目的一个核心部分的简化编写,tcmalloc源码github链接如下:

GitHub - gperftools/gperftools: Main gperftools repository

项目涉及知识

互斥锁、单例模式、C\C++代码运用、数据结构(哈希、链表)、多线程、模板、异常处理等

项目原理

内存池可以理解成是一个存储内存的池子,我们可以先向内存申请空间来组成内存池,当用户需要申请空间时,就不用向操作系统申请内存,而是直接从内存池中取出内存。

高并发内存池旨在通过控制多线程进行内存申请,并控制多个线程多并发时不会产生冲突,我们需要考虑线程间的冲突、性能问题及内存碎片化的问题。

内存碎片化:内存碎片化问题在内存池中分为 内碎片和外碎片
内碎片:在申请出来的空间中,如果有用不上的内存空间,我们就叫它内碎片。(例如:线程向内存池申请了20字节的空间,但实际上只用了16字节,那么剩下的4字节就叫做内碎片)
外碎片:在内存释放后,无法和其他内存连接起来(不连续)导致使用不上的内存,就叫做外碎片(例如:释放了一个10字节的空间,和另一个与这个空间不连续的10字节空间,当再次申请15字节时就无法申请,因为这两个空间不连续,没有办法作为一个大块切分成15字节给用户)

本项目重点由三个缓存组成:

thread cache:线程缓存是每个线程独有的,线程缓存内部最多可以申请到256KB内存,这个缓存是每个线程独有的,所以不需要进行加锁。

central cache:中心缓存所有的线程共享,当线程缓存中内存不足分配时,线程会到中心缓存中按照需求取一定的空间(这个空间叫span,后面会完善span的概念),同时,中心缓存也会在thread cache中内存太多的时候回收内存,避免一个线程中占用内存太多又不用,导致其他线程内存不足。因为中心缓存是所有线程共享的,所以在线程进入中心缓存后,我们需要加锁,在中心缓存中这个锁叫做“桶锁”,之后会进行更详细的说明。

page cache:页缓存同样也是所有线程共享的,因为其特殊的机制,当线程访问页缓存时,需要对页缓存的整个结构上锁。当中心缓存中内存不够时,也需要向页缓存申请内存,页缓存内存不够就需要向操作系统申请内存。同时,在释放内存时,页缓存会接收到中心缓存释放下来的内存,并对内存进行管理,将不连续的内存合并,消除内存的外碎片化。

项目实现

common.h

在项目中,我们需要一个common头文件来展开库并进行一些数据结构的定义,并且会进行一些算法的写入,common头文件中的内容会在程序编写中逐渐扩展,具体头文件内的结构,可以参考文末的源码以及总体的思维导图。

thread cache

thread cache实现的主要数据结构是一个哈希桶,桶内存储不同字节的空间,且在桶内的每个单链表中的每一块的前方都会用一个指针的空间存储下一个内存空间的地址。

为了保证每一个线程都能有自己的线程缓存,我们需要引入线程局部存储的概念。

线程局部存储(TLS):TLS的全称是thread local storage,它是一种变量的存储方法,这个变量在它所在的线程内是全局可访问的,但是不能被其他线程访问到,以保持数据的线程独立性。
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
在这个程序中,每次访问thread cache时都要检查这个TLS是否为空,如果为空则新建一个thread cache,否则就代表该thread cache已经被使用。

thread cache头文件中的函数及意义:

在thread cache中申请内存:

  1. 用户申请内存时(字节数小于等于256KB)通过一个中间函数调用thread cache,优先从thread cache中取出内存给用户

  1. 当thread cache中内存不足时,需要向中心缓存(central cache)批量申请所需对齐字节的内存

  1. thread cache取内存的动作就是将哈希桶对应字节的链表里面挂的内存头删出来

在thread cache中释放内存:

  1. 当thread cache中内存挂太多的时候,会向中心缓存释放内存,回收一部分内存到中心缓存中

  1. 释放内存则是将释放回来的内存push到对应字节的链表中

central cache

central cache和thread cache一样,也是一个哈希桶的数据结构,不同的是,在中心缓存的链表中,挂的是一个叫“span”的东西,它由页为单位组成,有的是一页空间的span、有的是两页空间的span(这里的页为8K,即8*1024字节),同时span中又挂了一个一个小空间,哈希桶前面的字节就代表span中切分小内存的字节大小。

span中的链表需要用一个计数器进行控制,以便于内存的回收和利用。同时,因为规定了一个span被切成多少个小块,并且里面的小块都规定属于哪个span,为了方便回收,span中的链表结构是一个带头双向链表

由于central cache被每个线程共享,所以我们需要上锁,在这个场景中,线程冲突只会产生两个或多个线程在一个桶中取内存,或释放内存,所以我们需要只对桶上锁就好,这个锁就叫做“桶锁”(只需要在调用桶的地方上锁就行)

同时,因为central cache是单独的一个结构,所以我们需要使用单例模式来控制central cache只有一个对象。

单例模式:单例模式是指在整个系统生命周期内,保证一个类 只能产生一个实例,确保该类的唯一性,它需要私有构造函数、拷贝构造函数,创建全局静态变量作为调用改类的对象并写一个get单例的函数
单例模式大致分为 饿汉模式、懒汉模式两大类:
饿汉模式:该方法比较急切,所以它会在调用前就在全局中初始化好自己
懒汉模式:在获取单例对象的函数时,才会被创建,此时的全局静态变量必须为指针类型,在被创建前初始化为nullptr
在本项目中,我们使用 饿汉模式

central cache头文件中的函数及意义:

在central cache中申请内存:

  1. 中心缓存收到线程缓存的申请要求后,会在中心缓存中寻找上一级所需要的切好字节的span,至少获得一个span并返回一个对象指针,指向申请后的内存空间,取出的内存空间会对导致span中的计数器++,代表取出了多少内存

  1. 如果一个span都没有获得,则需要取page cache中取span,然后按照需要的字节再进行切分,通过再调用一次该函数来返回span(减少代码重用)

3.在线程缓存取内存的过程中,我们使用的慢开始算法,防止一次取太多内存

在central cache中释放内存:

1.当线程缓存中的内存太多时,会返回给中心缓存中的span,span中的计数器会--,当这个计数器减到0时,则可以返回给page cache,在page cache中进行内存合并

page cache

page cache是三层缓存中最接近操作系统的一层,它通过从堆中申请内存来获得span,并挂起。page cache的数据结构同样也是哈希桶,但哈希桶中挂的条件不再是字节,而是页。

在page cache中申请内存:

  1. 在页缓存中申请内存的流程如上图所示,如果对应的页数中有span,则直接弹出,给中心缓存。但如果没有span,就需要去更大的地方取,并将该span切分,然后挂到切分后对应的哈希桶中。

  1. 为了更好的对span进行管理,我们会需要有span和页号的对应关系

在page cache中释放内存:

1.释放内存时,需要对该span的前后页号进行查找,如果前或后方的页号为空,则需要将他们合并,以解决内存外碎片的问题

page cache头文件中的函数及意义:

项目源码

本文简单的对该项目的三层缓存进行了描述,更多的细节及代码请访问:

高并发内存池项目: 基于谷歌tcMalloc的简单高并发内存池项目 (gitee.com)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值