【项目日记】高并发内存池 ---项目介绍及组件定长池的实现

2024-08-26 23:36

本文主要是介绍【项目日记】高并发内存池 ---项目介绍及组件定长池的实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在这里插入图片描述

余生还长,你别慌,也别回头,别念旧.
--- 余华 ---

1 高并发内存池简介

高并发内存池项目是实现一个高并发的内存池,他的原型是google的一个开源项目tcmalloctcmalloc全称Thread-Caching Malloc,即线程缓存的malloc,实现了高效的多线程内存管理,用于替代系统的内存分配相关的函数malloc free

这个项目是把tcmalloc最核心的框架简化后拿出来,模拟实现出一个自己的高并发内存池,目的是为了学习tcamlloc项目的精华,谷歌大厂的项目那必是含金量十足!这种方式有点类似我们之前学习STL容器的方式。但是相比STL容器部分,tcmalloc的代码量和复杂度上升了很多,大家做好心理准备。

难度的上升,我们的收获和成长也是在这个过程中同步上升。

·tcmalloc·是大厂google开源的,可以认为当时顶尖的C++高手写出来的,他的知名度也是非常高的,不少公司都在用它,Go语言直接用它做了自己内存分配器。所以这个项目可以称之为c++学习者的必学项目!

很多程序员是熟悉这个项目的,那么有好处,也有坏处。

  • 好处就是把这个项目理解扎实了,会很受面试官的认可。
  • 坏处就是面试官可能也比较熟悉项目,对项目会问得比较深,比较细。如果你对项目掌握得不扎实,那么就容易碰钉子。

有兴趣可以来看看源码哦:tcmalloc源代码在这里

涉及的技术栈有以下:

  1. 多线程编程:
    • 线程安全:确保在多线程环境下内存分配和释放操作的安全性。
    • 锁机制:使用各种锁(如自旋锁、互斥锁等)来同步对共享资源的访问。
    • 原子操作:利用原子操作来保证数据的一致性和线程安全。
  2. 内存管理:
    • 内存分配策略:设计高效的内存分配算法,减少内存碎片。
    • 内存池:预先分配一大块内存,按需分配给用户,减少系统调用开销。
    • 缓存机制:使用线程局部缓存来提高内存分配和释放的效率。
  3. 数据结构与算法:
    • 链表:管理空闲内存块。
    • 哈希表:快速查找和定位内存块。
    • 树结构:如二叉树、B树等,用于组织和管理内存块。
  4. 操作系统知识:
    • 虚拟内存管理:理解操作系统的内存管理机制。
    • 系统调用:如mmap、munmap等,用于直接操作内存。
  5. 编程语言:
    • 当然是C++:tmalloc通常是用C或C++编写的,因为这些语言提供了接近硬件的编程能力。

2 定长池的实现

我们先来实现一个高并发内存池的小组件 — 定长池 ,来练练手!
定长池也是基于池化技术:

所谓“池化技术”,就是程序先向系统申请过量的资源,然后自己管理,以备不时之需。之所以要申请过量的资源,是因为每次申请该资源都有较大的开销,不如提前申请好了,这样使用时就会变得非常快捷,大大提高程序运行效率

2.1 定长池的设计理念

直接使用C++的new delete或者是C语言的malloc free都会出现一种问题:内存碎片!
当我们在堆上开辟出许多空间,然后随机释放掉一些空间,虽然我们会得到很多空间,但是此时的空间就很可能会死不连续的了,不能继续开辟出更大的空间了:
在这里插入图片描述

而定长池内部有一块连续的大空间和一个自由链表,每次开辟是都会在自由链表中先进行选择可以使用的空间块,如果没有就在大空间中进行取出一部分进行使用!

2.2 框架搭建

定长池需要:

  1. 一段连续的大空间:我们使用char*来进行管理,方便一个一个字节来进行处理。
  2. 一个自由链表:进行资源的回收使用,内部通过链表结构链接起来
  3. 剩余资源数:进行资源空间的管理,不足够是进行扩容!
//--- 定长池的实现 ---#include<iostream>
#include<vector>
#include<memory>using std::cout;
using std::endl;template<class T>
class ObjectPool
{
public:T* New(){}void Delete(T*obj){}private://需要一个大空间char* _memory = nullptr;//回收资源的链表void* _freelist = nullptr;//剩余字节数!size_t _remainBytes = 0;
};

2.3 New 与 Delete

New函数的书写逻辑是:

  1. 如果是第一次进行new,那么就开辟一个大空间,然后取出需要的空间进行返回
  2. 如果不是第一次开辟,那么就要进行一个选择,如果自由链表中有内存块,就直接拿来使用。如果没有就在大空间中取出一部分进行使用!
  3. 每次开辟都要对剩余容量进行处理!

需要注意的是进行强制类型转换我们使用c++11通过的新接口来确保安全!

T* New()
{T* obj = nullptr;//先从自由链表中获取if (_freelist){obj = reinterpret_cast<T*>(_freelist);//进行头删//使用二级指针 取出指针的空间进行操作void* next = *reinterpret_cast<void**>(_freelist);_freelist = next;return obj;}//如果是第一次创建,那么_memory为空if (_remainBytes < sizeof(T)){//开辟大空间进行使用_remainBytes = 128 * 1024;//_memory = reinterpret_cast<char*>( malloc (_remainBytes ) )//直接使用系统调用进行优化_memory = reinterpret_cast<char*>( SystemAlloc (_remainBytes >> 13) );//开辟失败抛出异常if (_memory == nullptr){throw std::bad_alloc();}}obj = nullptr;//取出对应数量的空间obj = reinterpret_cast<T*>(_memory);//每次至少分配一个指针的大小,方便自由链表的使用size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);//调整剩余参数_memory += objSize;_remainBytes -= objSize;//显示调用构造函数new(obj)T;return obj;
}

Delete函数的逻辑先将资源进行析构,然后不能将空间直接进行释放,而是将该空间直接头插到自由链表中!

特别需要注意的是头插的环节,如果是第一次删除,就直接等于就可以。反之就需要进行头插,需要先在空间中取出一个指针的空间来指向下一空间!!!

取指针空间的操作要使用二级指针来进行!使用一级指针(int*)是不合适的,因为一级指针解引用不能适配32位和64位(除非进行一些特殊判断,比较麻烦)。使用二级指针void**,解引用会直接取出void*的大小,真好就是对应指针的大小,就可以进行取出使用了!

void Delete(T*obj)
{//显示调用进行析构obj->~T();//将删除的空间插入到自由链表中//第一次进行插入if (_freelist == nullptr){_freelist = obj;*reinterpret_cast<void**>(_freelist) = nullptr;}//不是第一次就进行头插else{*reinterpret_cast<void**>(obj) = _freelist;_freelist = obj;}
}

2.4 系统调用优化

为了追求更高的效率,我们可以使用底层的系统调用来进行:
这样就需要对不同的操作系统进行区分处理了!只针对特定场景进行处理,不在进行maloc,直接按页传递内存!

#ifdef _WIN32#include<windows.h>
#else
// linux
#endifusing std::cout;
using std::endl;// 直接去堆上按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else// linux下brk mmap等
#endifif (ptr == nullptr)throw std::bad_alloc();return ptr;
}

3 性能测试

我们完成了定长池的书写,接下来我们来看看他的效率怎么样!!!

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<TreeNode*> 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<TreeNode*> v2;v2.reserve(N);ObjectPool<TreeNode> 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;
}

可以看到效率是快了10倍啊!!!很好很好!!!

在这里插入图片描述
这样定长池就完成了

定长池中最关键的有三点:

  1. 在32位 / 64位系统下取出一个指针的空间,使用二级指针,二级指针解引用一定是当前环境下只指针的大小!
  2. 自由链表的内存块的链接,需要特别注意,其中没有多加指针,而是是否巧妙的利用类型转换进行使用!
  3. 如何实现的内存分配以及资源复用?通过大空间的切割和自由链表的回收!!!

这篇关于【项目日记】高并发内存池 ---项目介绍及组件定长池的实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1109999

相关文章

部署Vue项目到服务器后404错误的原因及解决方案

《部署Vue项目到服务器后404错误的原因及解决方案》文章介绍了Vue项目部署步骤以及404错误的解决方案,部署步骤包括构建项目、上传文件、配置Web服务器、重启Nginx和访问域名,404错误通常是... 目录一、vue项目部署步骤二、404错误原因及解决方案错误场景原因分析解决方案一、Vue项目部署步骤

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

如何通过Python实现一个消息队列

《如何通过Python实现一个消息队列》这篇文章主要为大家详细介绍了如何通过Python实现一个简单的消息队列,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录如何通过 python 实现消息队列如何把 http 请求放在队列中执行1. 使用 queue.Queue 和 reque

Python如何实现PDF隐私信息检测

《Python如何实现PDF隐私信息检测》随着越来越多的个人信息以电子形式存储和传输,确保这些信息的安全至关重要,本文将介绍如何使用Python检测PDF文件中的隐私信息,需要的可以参考下... 目录项目背景技术栈代码解析功能说明运行结php果在当今,数据隐私保护变得尤为重要。随着越来越多的个人信息以电子形

使用 sql-research-assistant进行 SQL 数据库研究的实战指南(代码实现演示)

《使用sql-research-assistant进行SQL数据库研究的实战指南(代码实现演示)》本文介绍了sql-research-assistant工具,该工具基于LangChain框架,集... 目录技术背景介绍核心原理解析代码实现演示安装和配置项目集成LangSmith 配置(可选)启动服务应用场景

使用Python快速实现链接转word文档

《使用Python快速实现链接转word文档》这篇文章主要为大家详细介绍了如何使用Python快速实现链接转word文档功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 演示代码展示from newspaper import Articlefrom docx import

前端原生js实现拖拽排课效果实例

《前端原生js实现拖拽排课效果实例》:本文主要介绍如何实现一个简单的课程表拖拽功能,通过HTML、CSS和JavaScript的配合,我们实现了课程项的拖拽、放置和显示功能,文中通过实例代码介绍的... 目录1. 效果展示2. 效果分析2.1 关键点2.2 实现方法3. 代码实现3.1 html部分3.2

golang内存对齐的项目实践

《golang内存对齐的项目实践》本文主要介绍了golang内存对齐的项目实践,内存对齐不仅有助于提高内存访问效率,还确保了与硬件接口的兼容性,是Go语言编程中不可忽视的重要优化手段,下面就来介绍一下... 目录一、结构体中的字段顺序与内存对齐二、内存对齐的原理与规则三、调整结构体字段顺序优化内存对齐四、内

Java深度学习库DJL实现Python的NumPy方式

《Java深度学习库DJL实现Python的NumPy方式》本文介绍了DJL库的背景和基本功能,包括NDArray的创建、数学运算、数据获取和设置等,同时,还展示了如何使用NDArray进行数据预处理... 目录1 NDArray 的背景介绍1.1 架构2 JavaDJL使用2.1 安装DJL2.2 基本操