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

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

相关文章

SpringBoot使用ffmpeg实现视频压缩

《SpringBoot使用ffmpeg实现视频压缩》FFmpeg是一个开源的跨平台多媒体处理工具集,用于录制,转换,编辑和流式传输音频和视频,本文将使用ffmpeg实现视频压缩功能,有需要的可以参考... 目录核心功能1.格式转换2.编解码3.音视频处理4.流媒体支持5.滤镜(Filter)安装配置linu

在Spring Boot中实现HTTPS加密通信及常见问题排查

《在SpringBoot中实现HTTPS加密通信及常见问题排查》HTTPS是HTTP的安全版本,通过SSL/TLS协议为通讯提供加密、身份验证和数据完整性保护,下面通过本文给大家介绍在SpringB... 目录一、HTTPS核心原理1.加密流程概述2.加密技术组合二、证书体系详解1、证书类型对比2. 证书获

Druid连接池实现自定义数据库密码加解密功能

《Druid连接池实现自定义数据库密码加解密功能》在现代应用开发中,数据安全是至关重要的,本文将介绍如何在​​Druid​​连接池中实现自定义的数据库密码加解密功能,有需要的小伙伴可以参考一下... 目录1. 环境准备2. 密码加密算法的选择3. 自定义 ​​DruidDataSource​​ 的密码解密3

使用Python实现Windows系统垃圾清理

《使用Python实现Windows系统垃圾清理》Windows自带的磁盘清理工具功能有限,无法深度清理各类垃圾文件,所以本文为大家介绍了如何使用Python+PyQt5开发一个Windows系统垃圾... 目录一、开发背景与工具概述1.1 为什么需要专业清理工具1.2 工具设计理念二、工具核心功能解析2.

Maven项目中集成数据库文档生成工具的操作步骤

《Maven项目中集成数据库文档生成工具的操作步骤》在Maven项目中,可以通过集成数据库文档生成工具来自动生成数据库文档,本文为大家整理了使用screw-maven-plugin(推荐)的完... 目录1. 添加插件配置到 pom.XML2. 配置数据库信息3. 执行生成命令4. 高级配置选项5. 注意事

C++ RabbitMq消息队列组件详解

《C++RabbitMq消息队列组件详解》:本文主要介绍C++RabbitMq消息队列组件的相关知识,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录1. RabbitMq介绍2. 安装RabbitMQ3. 安装 RabbitMQ 的 C++客户端库4. A

Java实现本地缓存的常用方案介绍

《Java实现本地缓存的常用方案介绍》本地缓存的代表技术主要有HashMap,GuavaCache,Caffeine和Encahche,这篇文章主要来和大家聊聊java利用这些技术分别实现本地缓存的方... 目录本地缓存实现方式HashMapConcurrentHashMapGuava CacheCaffe

SpringBoot整合Sa-Token实现RBAC权限模型的过程解析

《SpringBoot整合Sa-Token实现RBAC权限模型的过程解析》:本文主要介绍SpringBoot整合Sa-Token实现RBAC权限模型的过程解析,本文给大家介绍的非常详细,对大家的学... 目录前言一、基础概念1.1 RBAC模型核心概念1.2 Sa-Token核心功能1.3 环境准备二、表结

Python实现一键PDF转Word(附完整代码及详细步骤)

《Python实现一键PDF转Word(附完整代码及详细步骤)》pdf2docx是一个基于Python的第三方库,专门用于将PDF文件转换为可编辑的Word文档,下面我们就来看看如何通过pdf2doc... 目录引言:为什么需要PDF转Word一、pdf2docx介绍1. pdf2docx 是什么2. by

eclipse如何运行springboot项目

《eclipse如何运行springboot项目》:本文主要介绍eclipse如何运行springboot项目问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目js录当在eclipse启动spring boot项目时出现问题解决办法1.通过cmd命令行2.在ecl