从零开始实现一个可靠、健壮的内存池

2024-06-02 23:04

本文主要是介绍从零开始实现一个可靠、健壮的内存池,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 概要
      • 这个项目是干什么的
      • 项目所需储备知识
    • 什么是内存池
      • 池化技术
      • 内存池
      • 内存池主要解决的问题
    • 框架设计
    • 开发计划
    • 系统测试情况
    • 遇到的主要问题和解决方法
    • 分工和协作
    • 提交仓库目录和文件描述
    • 比赛收获

概要

这个项目是干什么的

当前项目是实现一个高并发的内存池,他的原型是google的一个开源项目tcmalloc,tcmalloc全称 Thread-Caching Malloc,即线程缓存的malloc,实现了高效的多线程内存管理,用于替代系统的内存 分配相关的函数(malloc、free)。 这个项目是把tcmalloc最核心的框架简化后拿出来,模拟实现出一个自己的高并发内存池,目的就 是学习tcamlloc的精华,这种方式有点类似我们之前学习STL容器的方式。


项目所需储备知识

这个项目会用到C/C++、数据结构(链表、哈希桶)、操作系统内存管理、单例模式、多线程、互斥锁 等等方面的知识。


什么是内存池

池化技术

所谓“池化技术”,就是程序先向系统申请过量的资源,然后自己管理,以备不时之需。之所以要申请过 量的资源,是因为每次申请该资源都有较大的开销,不如提前申请好了,这样使用时就会变得非常快 捷,大大提高程序运行效率。 在计算机中,有很多使用“池”这种技术的地方,除了内存池,还有连接池、线程池、对象池等。以服务 器上的线程池为例,它的主要思想是:先启动若干数量的线程,让它们处于睡眠状态,当接收到客户端 的请求时,唤醒池中某个睡眠的线程,让它来处理客户端的请求,当处理完这个请求,线程又进入睡眠 状态。

内存池

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

内存池主要解决的问题

内存池解决的主要是效率及内存碎片问题。内存碎片分为内碎片/外碎片。

外部碎片是一些空闲的 连续内存区域太小,这些内存空间不连续,以至于合计的内存足够,但是不能满足一些的内存分配申请 需求。内部碎片是由于一些对齐的需求,导致分配出去的空间中一些内存无法被利用。


架构设计

现代很多的开发环境都是多核多线程,在申请内存的场景下,必然存在激烈的锁竞争问题。malloc本身 其实已经很优秀,那么我们项目的原型tcmalloc就是在多线程高并发的场景下更胜一筹,所以这次我们 实现的内存池需要考虑以下几方面的问题。

1. 性能问题。

2. 多线程环境下,锁竞争问题。

3. 内存碎片问题。

 concurrent memory pool主要由以下3个部分构成:

1. thread cache:线程缓存是每个线程独有的,用于小于256KB的内存的分配,线程从这里申请内 存不需要加锁,每个线程独享一个cache,这也就是这个并发线程池高效的地方。

2. central cache:中心缓存是所有线程所共享,thread cache是按需从central cache中获取的对 象。central cache合适的时机回收thread cache中的对象,避免一个线程占用了太多的内存,而 其他线程的内存吃紧,达到内存分配在多个线程中更均衡的按需调度的目的。central cache是存 在竞争的,所以从这里取内存对象是需要加锁,首先这里用的是桶锁,其次只有thread cache的 没有内存对象时才会找central cache,所以这里竞争不会很激烈。

3. page cache:页缓存是在central cache缓存上面的一层缓存,存储的内存是以页为单位存储及分 配的,central cache没有内存对象时,从page cache分配出一定数量的page,并切割成定长大小 的小块内存,分配给central cache。当一个span的几个跨度页的对象都回收以后,page cache 会回收central cache满足条件的span对象,并且合并相邻的页,组成更大的页,缓解内存碎片 的问题。

 


开发计划

开发计划为

1、先实现由ThreadCache至PageCache的内存申请过程。再进行简单的调试,对申请内存进行联调

2、再实现由ThreadCache至PageCache的内存释放过程,进行简单的调试,对释放内存进行联调

3、进行性能测试

4、思考优化,如利用定长内存池代替其中的new,delete操作,释放内存时不用带对象大小等


系统测试情况

以下为多线程并发环境下,对比malloc和ConcurrentAlloc申请和释放内存效率对比

void BenchmarkMalloc(size_t ntimes, size_t nworks, size_t rounds)
{std::vector<std::thread> vthread(nworks);std::atomic<size_t> malloc_costtime = 0;std::atomic<size_t> free_costtime = 0;for (size_t k = 0; k < nworks; ++k){vthread[k] = std::thread([&, k]() {std::vector<void*> v;v.reserve(ntimes);for (size_t j = 0; j < rounds; ++j){size_t begin1 = clock();for (size_t i = 0; i < ntimes; i++){v.push_back(malloc(16));//v.push_back(malloc((16 + i) % 8192 + 1));}size_t end1 = clock();size_t begin2 = clock();for (size_t i = 0; i < ntimes; i++){free(v[i]);}size_t end2 = clock();v.clear();malloc_costtime += (end1 - begin1);free_costtime += (end2 - begin2);}});}for (auto& t : vthread){t.join();}printf("%u个线程并发执行%u轮次,每轮次malloc %u次: 花费:%u ms\n",nworks, rounds, ntimes, malloc_costtime);printf("%u个线程并发执行%u轮次,每轮次free %u次: 花费:%u ms\n",nworks, rounds, ntimes, free_costtime);printf("%u个线程并发malloc&free %u次,总计花费:%u ms\n",nworks, nworks*rounds*ntimes, malloc_costtime + free_costtime);
}

遇到的主要问题和解决方法

问题

  1. 多线程环境下的锁竞争问题:多个线程同时访问内存池时,如何减少锁竞争以提高性能。
  2. 平台及兼容性在不同操作系统和架构下,内存分配和管理的差异可能导致兼容性问题。
  3. 内存池自身数据结构的管理:内存池自身数据结构(如SpanList中的span等)的管理也可能使用到malloc,没有完全脱离malloc。

解决办法

  1. 减少锁竞争
    • 通过为每个线程分配独立的threadCache,减少多线程环境下的锁竞争。
    • 在centralCache中使用桶锁(bucket lock)等技术,进一步减少锁竞争。
  2. 平台及兼容性处理
    • 根据不同平台和架构的特性,选择合适的内存分配和管理策略。
    • 在Linux等系统下,将某些特定的内存分配函数(如VirtualAlloc)替换为brk等。
  3. 内存池自身数据结构的管理
    • 对于内存池自身数据结构的管理,尽量减少使用malloc和new,可以考虑使用其他方式(如virtual alloc、brk、mmap等)来申请大块内存,并使用对象池等技术来管理小块内存。
    • 在64位系统下,对于某些数据结构(如map<id, Span*>)可能存在的性能和内存问题,可以考虑使用基数树等更高效的数据结构进行替换。

分工和协作

 项目规划与定义

  • 项目经理
    • 定义项目目标、范围和里程碑。
    • 制定项目计划和时间表。
    • 分配资源和任务给团队成员。
  • 架构师
    • 设计项目的整体架构,包括内存池的设计、线程缓存(ThreadCache)、中心缓存(CentralCache)和页缓存(PageCache)的交互方式。
    • 评估技术选型,确保所选技术栈能够支持高并发场景。

2. 编码实现

  • 核心开发团队
    • 负责实现内存池的核心功能,如内存的申请、分配、释放和合并。
    • 编写单元测试,确保每个模块的正确性。
    • 协作进行代码审查,提高代码质量。
    分工示例
    • 开发者A:负责ThreadCache的实现,优化线程间的内存访问性能。
    • 开发者B:负责CentralCache的实现,确保多个线程能够高效共享内存资源。
    • 开发者C:负责PageCache的实现,处理大内存块的分配和回收。
  • 性能测试团队
    • 设计并执行性能测试,评估内存池的性能和并发能力。
    • 根据测试结果提供优化建议。

3. 并发与锁优化

  • 并发控制专家
    • 负责优化多线程环境下的锁竞争问题,提高内存池的并发性能。
    • 研究并使用先进的并发控制算法,如无锁编程技术。

提交仓库目录和文件描述

文件描述

三层缓存结构

        ThreadCache层:ThreadCache.h,ThreadCache.cpp

        CentralCache层:CentralCache.h,CentralCache.cpp

        PageCache层:PageCache.h,PageCache,cpp

用于所有文件公用的类/变量

        common.h

用于替代new/delete的定长内存池

        ObjectPool.h

用于进行性能测试的文件

        Benchmark.cpp

核心接口

        ConcurrentAlloc.cpp

比赛收获

1. 深入理解高并发与内存管理

  • 通过设计高并发内存池,我深入理解了在高并发环境下,如何有效地管理内存资源,包括内存的分配、回收和复用,以及如何在多线程环境中确保内存操作的安全性和高效性。
  • 我学会了如何分析并解决内存碎片化问题,这对于提高系统的性能和稳定性至关重要。

2. 掌握了先进的并发控制技术

  • 在设计过程中,我接触并掌握了多种先进的并发控制技术,如无锁编程、锁分离、读写锁等,这些技术对于提高内存池的并发性能至关重要。
  • 我学会了如何根据具体的业务场景和需求,选择合适的并发控制技术,以达到最佳的性能和效率。

3. 提升了系统设计和架构能力

  • 设计高并发内存池需要综合考虑多个方面,包括系统的整体架构、模块划分、接口设计、线程模型等。通过这次经历,我提升了系统设计和架构能力,学会了如何构建一个高性能、可扩展、易维护的系统。
  • 我学会了如何平衡系统的复杂性和性能之间的关系,以及如何在设计过程中考虑系统的可测试性和可维护性。

4. 加强了团队协作和沟通能力

  • 在设计高并发内存池的过程中,我与团队成员进行了密切的协作和沟通,共同解决了许多技术难题。这次经历加强了我的团队协作和沟通能力,使我更加擅长与团队成员合作,共同完成任务。
  • 我学会了如何有效地表达自己的观点和想法,以及如何倾听他人的意见和建议,这对于提高团队的凝聚力和工作效率至关重要。

5. 增强了解决问题的能力

  • 在设计过程中,我遇到了许多复杂的问题和挑战,如内存泄漏、死锁、性能瓶颈等。通过不断地尝试和探索,我逐渐学会了如何分析问题、定位问题并解决问题。
  • 这次经历增强了我的解决问题的能力,使我更加自信和从容地面对未来的技术挑战。

6. 拓展了技术视野和知识面

  • 在设计高并发内存池的过程中,我接触了许多新的技术和工具,如高性能数据结构、并发编程库、性能测试工具等。这些新的技术和工具拓展了我的技术视野和知识面,使我对计算机系统的底层原理和性能优化有了更深入的理解。

总之,参加这次比赛是一次非常有价值的经历。通过这次经历,我不仅提升了自己的技术能力和团队协作能力,还拓展了自己的技术视野和知识面。我相信这些收获将对我未来的职业发展产生积极的影响。

这篇关于从零开始实现一个可靠、健壮的内存池的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

Java中使用Java Mail实现邮件服务功能示例

《Java中使用JavaMail实现邮件服务功能示例》:本文主要介绍Java中使用JavaMail实现邮件服务功能的相关资料,文章还提供了一个发送邮件的示例代码,包括创建参数类、邮件类和执行结... 目录前言一、历史背景二编程、pom依赖三、API说明(一)Session (会话)(二)Message编程客

Java中List转Map的几种具体实现方式和特点

《Java中List转Map的几种具体实现方式和特点》:本文主要介绍几种常用的List转Map的方式,包括使用for循环遍历、Java8StreamAPI、ApacheCommonsCollect... 目录前言1、使用for循环遍历:2、Java8 Stream API:3、Apache Commons

C#提取PDF表单数据的实现流程

《C#提取PDF表单数据的实现流程》PDF表单是一种常见的数据收集工具,广泛应用于调查问卷、业务合同等场景,凭借出色的跨平台兼容性和标准化特点,PDF表单在各行各业中得到了广泛应用,本文将探讨如何使用... 目录引言使用工具C# 提取多个PDF表单域的数据C# 提取特定PDF表单域的数据引言PDF表单是一

使用Python实现高效的端口扫描器

《使用Python实现高效的端口扫描器》在网络安全领域,端口扫描是一项基本而重要的技能,通过端口扫描,可以发现目标主机上开放的服务和端口,这对于安全评估、渗透测试等有着不可忽视的作用,本文将介绍如何使... 目录1. 端口扫描的基本原理2. 使用python实现端口扫描2.1 安装必要的库2.2 编写端口扫

Java循环创建对象内存溢出的解决方法

《Java循环创建对象内存溢出的解决方法》在Java中,如果在循环中不当地创建大量对象而不及时释放内存,很容易导致内存溢出(OutOfMemoryError),所以本文给大家介绍了Java循环创建对象... 目录问题1. 解决方案2. 示例代码2.1 原始版本(可能导致内存溢出)2.2 修改后的版本问题在

PyCharm接入DeepSeek实现AI编程的操作流程

《PyCharm接入DeepSeek实现AI编程的操作流程》DeepSeek是一家专注于人工智能技术研发的公司,致力于开发高性能、低成本的AI模型,接下来,我们把DeepSeek接入到PyCharm中... 目录引言效果演示创建API key在PyCharm中下载Continue插件配置Continue引言

MySQL分表自动化创建的实现方案

《MySQL分表自动化创建的实现方案》在数据库应用场景中,随着数据量的不断增长,单表存储数据可能会面临性能瓶颈,例如查询、插入、更新等操作的效率会逐渐降低,分表是一种有效的优化策略,它将数据分散存储在... 目录一、项目目的二、实现过程(一)mysql 事件调度器结合存储过程方式1. 开启事件调度器2. 创

使用Python实现操作mongodb详解

《使用Python实现操作mongodb详解》这篇文章主要为大家详细介绍了使用Python实现操作mongodb的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、示例二、常用指令三、遇到的问题一、示例from pymongo import MongoClientf

SQL Server使用SELECT INTO实现表备份的代码示例

《SQLServer使用SELECTINTO实现表备份的代码示例》在数据库管理过程中,有时我们需要对表进行备份,以防数据丢失或修改错误,在SQLServer中,可以使用SELECTINT... 在数据库管理过程中,有时我们需要对表进行备份,以防数据丢失或修改错误。在 SQL Server 中,可以使用 SE

基于Go语言实现一个压测工具

《基于Go语言实现一个压测工具》这篇文章主要为大家详细介绍了基于Go语言实现一个简单的压测工具,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录整体架构通用数据处理模块Http请求响应数据处理Curl参数解析处理客户端模块Http客户端处理Grpc客户端处理Websocket客户端