加入收藏 | 设为首页 | 会员中心 | 我要投稿 拼字网 - 核心网 (https://www.hexinwang.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 服务器 > 搭建环境 > Linux > 正文

实战项目:手把手带你实现一个高并发内存池

发布时间:2022-09-24 15:16:32 所属栏目:Linux 来源:
导读:  项目介绍

  1.这个项目做的是什么?

  当前项目是实现一个高并发的内存池,他的原型是google的一个开源项目tcmalloc,tcmalloc全称Thread-Caching Malloc,即线程缓存的malloc,实现了高效的多线程内
  项目介绍
 
  1.这个项目做的是什么?
 
  当前项目是实现一个高并发的内存池,他的原型是google的一个开源项目tcmalloc,tcmalloc全称Thread-Caching Malloc,即线程缓存的malloc,实现了高效的多线程内存管理,用于替代系统的内存分配相关的函数(malloc、free)。
 
  2.项目目标
 
  模拟实现出一个自己的高并发内存池,在多线程环境下缓解了锁竞争问题,相比于malloc/free效率提高了25%左右,将内存碎片保持在10%左右。
 
  内存池介绍池化技术
 
  所谓“池化技术”,就是程序先向系统申请过量的资源,然后自己管理,以备不时之需。之所以要申请过量的资源,是因为每次申请该资源都有较大的开销,不如提前申请好了,这样使用时就会变得非常快捷,大大提高程序运行效率。
 
  在计算机中,有很多使用“池”这种技术的地方,除了内存池,还有连接池、线程池、对象池等。以服务器上的线程池为例,它的主要思想是:先启动若干数量的线程,让它们处于睡眠状态,当接收到客户端的请求时,唤醒池中某个睡眠的线程,让它来处理客户端的请求,当处理完这个请求,线程又进入睡眠状态。
 
  内存池
 
  内存池是指程序预先从操作系统申请一块足够大内存,此后,当程序中需要申请内存的时候,不是直接向操作系统申请,而是直接从内存池中获取;同理,当程序释放内存的时候,并不真正将内存返回给操作系统,而是返回内存池。当程序退出(或者特定时间)时,内存池才将之前申请的内存真正释放。
 
  内存池主要解决的问题
 
  内存池主要解决的当然是效率的问题,其次如果作为系统的内存分配器的角度,还需要解决一下内存碎片的问题。那么什么是内存碎片呢?
 
  定长内存池
 
  作为程序员(C/C++)我们知道申请内存使用的是malloc,malloc其实就是一个通用的大众货,什么场景下都可以用,但是什么场景下都可以用就意味着什么场景下都不会有很高的性能,下面我们就先来设计一个定长内存池做个开胃菜,当然这个定长内存池在我们后面的高并发内存池中也是有价值的,所以学习他目的有两层,先熟悉一下简单内存池是如何控制的,第二他会作为我们后面内存池的一个基础组件。
 
  定长内存池之所以高效:是因为它可以切除固定大小的内存,供线程使用。还可以回收,线程释放的内存链接在自由链表中,供下一次线程申请内存使用。
 
  代码展示
 
  #pragma once
  #include
  #include
  #include
  #include
  using std::cout;
  using std::endl;
  // 直接去堆上按页申请空间
  inline static void* SystemAlloc(size_t kpage)
  {
  void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
  if (ptr == nullptr)
  throw std::bad_alloc();
  return ptr;
  }
  template
  class ObjectPool
  {
  public:
  T* New()
  {
  T* obj = nullptr;
  // 优先把还回来内存块对象,再次重复利用
  if (_freeList)
  {
  //头删
  void* next = *((void**)_freeList);
  //将链表的第一个空间给obj使用,freeList存的就是第一个小内存的地址
  obj = (T*)_freeList;
  _freeList = next;
  }
  else
  {
  // 剩余内存不够一个对象大小时,则重新开大块空间
  if (_remainBytes < sizeof(T))
  {
  _remainBytes = 128 * 1024; //16页
  //_memory = (char*)malloc(_remainBytes);
  //SystemAlloc(x)直接向系统申请内存,x表示申请的页数
  _memory = (char*)SystemAlloc(_remainBytes >> 13); //申请16页
  if (_memory == nullptr)
  {
  throw std::bad_alloc();
  }
  }
  obj = (T*)_memory;
  //一个对象的大小 ,小于指针大小,就给一个指针大小
  size_t objSize = sizeof(T) < sizeof(void*) ?
  sizeof(void*) : sizeof(T);
  _memory += objSize; //指针往后走一个小块空间
  _remainBytes -= objSize; //每用一小块空间,剩余空间更新
  }
  // 定位new,显示调用T的构造函数初始化
  new(obj)T;
  return obj;
  }
  void Delete(T* obj)
  {
  // 显示调用析构函数清理对象
  obj->~T();
  // 头插,将不用的小块空间,插入自由链表中
  *(void**)obj = _freeList; //*(void**) 解引用拿到 void*,在32/64位下大小为 4/8
  _freeList = obj;
  }
  private:
  char* _memory = nullptr; // 指向大块内存的指针(向系统申请的大块内存)
  size_t _remainBytes = 0; // 大块内存在切分过程中剩余字节数
  void* _freeList = nullptr; // 还回来过程中链接的自由链表的头指针
  };
  struct TreeNode
  {
  int _val;
  TreeNode* _left;
  TreeNode* _right;
  TreeNode()
  :_val(0)
  , _left(nullptr)
  , _right(nullptr)
  {}
  };
  void TestObjectPool()
  {
  // 申请释放的轮次
  const size_t Rounds = 5;
  // 每轮申请释放多少次
  const size_t N = 100000;
  std::vector v1;
  v1.reserve(N);
  size_t begin1 = clock();
  for (size_t j = 0; j < Rounds; ++j)
  {
  for (int i = 0; i < N; ++i)
  {
  v1.push_back(new TreeNode);
  }
  for (int i = 0; i < N; ++i)
  {
  delete v1[i];
  }
  v1.clear();
  }
  size_t end1 = clock();
  std::vector v2;
  v2.reserve(N);
  ObjectPool TNPool;
  size_t begin2 = clock();
  for (size_t j = 0; j < Rounds; ++j)
  {
  for (int i = 0; i < N; ++i)
  {
  v2.push_back(TNPool.New());
  }
  for (int i = 0; i < N; ++i)
  {
  TNPool.Delete(v2[i]);
  }
  v2.clear();
  }
  size_t end2 = clock();
  cout << "new cost time:" << end1 - begin1 << endl;
  cout << "object pool cost time:" << end2 - begin2 << endl;
  }
  int main()
  {
  TestObjectPool();
  return 0;
  }
  效果演示
 
  可以看出,使用定长内存池,率率比使用malloc申请空间要高的多。
 
  相关视频推荐
 
  90分钟了解Linux内存架构,numa的优势,slab的实现,vmalloc原理
 
  内存泄漏的3个解决方案与原理实现,知道一个可以轻松应对开发
 
  学习地址:C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂
 
  需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
 
  高并发内存池整体框架设计
 
  现代很多的开发环境都是多核多线程,在申请内存的场景下,必然存在激烈的锁竞争问题。malloc本身其实已经很优秀,那么我们项目的原型tcmalloc就是在多线程高并发的场景下更胜一筹,所以这次我们实现的内存池需要考虑以下几方面的问题。
 
  1. 性能问题。
 
  2. 多线程环境下,锁竞争问题。
 
  3. 内存碎片问题。
 
  thread cache:线程缓存是每个线程独有的,用于小于256KB的内存的分配,线程从这里申请内存不需要加锁,每个线程独享一个cache,这也就是这个并发线程池高效的地方。
 
  central cache:中心缓存是所有线程所共享,thread cache是按需从central cache中获取的对象。central cache合适的时机回收thread cache中的对象,避免一个线程占用了太多的内存,而其他线程的内存吃紧,达到内存分配在多个线程中更均衡的按需调度的目的。central cache是存在竞争的,所以从这里取内存对象是需要加锁,首先这里用的是桶锁,其次只有thread cache的没有内存对象时才会找central cache,所以这里竞争不会很激烈。
 
  page cache:页缓存是在central cache缓存上面的一层缓存,存储的内存是以页为单位存储及分配的,central cache没有内存对象时,从page cache分配出一定数量的page,并切割成定长大小的小块内存,分配给central cache。当一个span的几个跨度页的对象都回收以后,page cache会回收central cache满足条件的span对象,并且合并相邻的页,组成更大的页,缓解内存碎片的问题。
 
  高并发内存池–thread cache
 
  thread cache是哈希桶结构线程池linux,每个桶是一个按桶位置映射大小的内存块对象的自由链表。每个线程都会有一个thread cache对象,这样每个线程在这里获取对象和释放对象时是无锁的。
 

(编辑:拼字网 - 核心网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!