【高并发内存池】第一篇:定长内存池设计

一. 什么是内存池?

1. 池化技术

内存池是池化技术的一种应用。所谓“池化技术”,就是程序先向系统申请过量的资源,然后自己管理,以备不时之需。之所以要申请过量的资源,是因为每次申请该资源都有较大的开销,不如提前申请好了,这样使用时就触手可及,大大提高程序运行效率。

生活中的池化技术

在现实生活,每个月父母都会通过微信或者支付宝转给我们一定数量的零花钱,这些“零花钱”就是我们向父母申请的资源,而微信钱包或者支付宝钱包就是存储我们所申请到资源的“零花钱池”,里面的资源可以任由我们分配。这样我们一个月中大大小小的生活开销每次只需从“零花钱池”中攫取即可,而不是每一次需要用到一点小钱就向先父母讨要,这样做效率是很低的。

计算机中的池化技术

在计算机中,还有很多使用“池”这种技术的地方,除了内存池,还有连接池、线程池、对象池等。以服务器中的线程池为例,它的思想是:先启动若干数量的线程,让它们处于休眠状态,当接收到客户端的请求时,便立即唤醒池中某个休眠的线程,让它来处理客户端的请求,当处理完这个请求后,线程再次进入休眠状态。

2. 内存池概念

内存池是指程序预先从操作系统申请一块足够大内存,此后当程序中需要再次申请内存的时候,不是直接向操作系统申请,而是直接从内存池中获取;同理,当程序释放内存的时候,并不真正将内存返回给操作系统,而是返回给内存池保管。当程序退出时,内存池才将之前所申请的内存归还给系统。

在 C 中我们要动态申请内存都是通过 malloc 去申请完成的,但是我们要知道,实际我们不是直接去堆中申请内存的,而是去 malloc 这个内存池中申请。malloc 函数相当于向操作系统“批发”了一块较大的内存空间,然后“零售”给程序用。当全部“售完”或程序有更大的内存需求时,再根据实际需求向操作系统“进货”。malloc 的内部实现方式有很多种,一般不同系统用的都是不一样的。比如 windows 的 vs 系列用的微软自己写的一套,linux gcc 用的是 glibc 中的 ptmalloc。

二. 为什么要有内存池?

通常我们习惯直接使用 new、malloc 等 API 去动态申请内存,但是这样做的缺点在于:由于所申请内存的大小不确定,频繁使用时会造成大量的内存碎片并进而降低性能。C/C++ 的内存分配(通过 malloc 或 new)需要花费一定的时间。更糟糕的是,随着时间的流逝,内存碎片会越积越多,所以一个应用程序运行了很长时间并执行了很多的内存分配(释放)操作的时候,它会越来越慢。

1. 内存碎片问题

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

  • 外碎片:由于多个空闲内存块的空间不连续,导致无法整合出一块更大更连续的内存空间,以至于最终不能满足一些更大的内存分配申请需求。
  • 内碎片:由于一些对齐的需求,导致分配出去的空间中一些内存无法被利用。

在这里插入图片描述

2. 内存池带来的好处

如果我们给一个程序设计一个它自己的内存池,这可以一定程度上缓解上面出现的问题。内存池可以给我们的程序带来以下两个好处:

  • 非常少(几乎没有) 内存碎片。
  • 速度比通常的内存申请/释放(malloc、 free)快。

这两个好处综合后可以显著提高程序的运行效率。

三. 定长内存池设计

1. 定长内存池特点

  • 每次只能申请、释放特定类型大小的内存。
  • 申请、释放内存的性能达到极致。
  • 不考虑内存碎片问题。

2. 定长内存池基本思想

第一步:首先我们向系统申请一段长度固定、连续的内存空间,用一个 char* 类型的指针变量 _memory 指向这段空间的起始地址:
在这里插入图片描述

第二步:每次程序可以从这段连续内存空间中申请特定类型:T,即大小为 sizeof(T) 字节的一块对象空间,申请完成后 _memory 往后移动 sizeof(T) 的距离,继续指向新的待申请空间的起始地址:
在这里插入图片描述

问题:为什么要把 _memory 的类型定义为 char*

首先我们要知道,对于一个指针变量来说,它的类型决定了它如下的两个特点:

  • 指针变量解引用时,所能获取到数据空间的大小
  • 指针变量 ++/-- 时,单步的步长。

在这里插入图片描述

程序申请走 sizeof(T) 个字节的对象后,_memory 应该往后走 sizeof(T) 个字节的长度,以指向新的待申请空间起始地址。我们把 _memory 定义成 char* 类型,这样单步就只走一个字节(方便控制),只需直接加上 sizeof(T) 就到了我们想要的位置。

补充:void* 类型的指针变量比较特别,它有如下几个特点:

  • 不能 +/- 整数
  • 不能解引用
  • 它可以接收所有指针类型的地址。
  • 使用时,一般是把它强转成其他类型的指针,然后再对其进行解引用、加减整数、赋值等操作。

第三步:释放一块不用的对象空间时,不能直接 free,而是应该把这块对象空间链接到一个链表中保存,这样下次再申请时就直接从链表中拿,而不用再去 _memory 中申请。这样可以加强内存空间的利用率。

那如何链接这些归还回来的对象呢?对于不再使用的对象空间我们让它的头 4 个或 8 个字节保存下一个节点的地址,这样每一块对象空间就以单链表的形式组织起来了:
在这里插入图片描述
我们定义一个指针变量 _freeList 去指向第一块对象空间的地址,类型定义成 void* 即可,这个指针变量仅仅起到一个哨兵位头节点的作用。

3. 定长内存池实现

3.1 基本框架

因为我们是针对特定大小空间来进行申请和释放操作的,所以我们把类名叫做对象池(ObjectPool):

template<class T>    
class ObjectPool    
{
   
       
  public:    
    // 对象池默认构造函数    
    ObjectPool()    
      :_memory(nullptr)    
      ,_remain(0)    
      ,_freeList(nullptr)    
    {
   
   }
    
  private:
    char* _memory;  // 对象池:指向一段连续待待分配的空间
    size_t _remain; // 记录对象池中剩余可用空间的大小,单位是字节
    void* _freeList;// 自由链表:指向第一块不用的对象空间
};    

3.2 释放(Delete)一块对象空间

调用者在外部把需要释放对象的地址传入,Delete() 内部负责把这块对象空间头插到自由链表中:

// 释放一块 T 类型的对象空间    
void Delete(T* obj)    
{
   
       
  // 1、显示调用对象类型的析构函数,完成对象空间内容的清理
  obj->~T();    
  // 2、把对象空间头插到自由链表中
  *(void**)obj = _freeList;    
  _freeList = obj;    
}  

补充说明

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值