百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

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

off999 2025-03-11 19:46 14 浏览 0 评论

项目介绍

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 < sizeoft _remainbytes='128' 1024 16 _memory='(char*)malloc(_remainBytes);' systemallocxx _memory='(char*)SystemAlloc(_remainBytes'>> 13); //申请16页
if (_memory == nullptr)
{
throw std::bad_alloc();
}
}
obj = (T*)_memory;
//一个对象的大小 ,小于指针大小,就给一个指针大小
size_t objSize = sizeof(T) < sizeofvoid sizeofvoid : sizeoft _memory _remainbytes -='objSize;' newt newobjt return obj void deletet 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是哈希桶结构,每个桶是一个按桶位置映射大小的内存块对象的自由链表。每个线程都会有一个thread cache对象,这样每个线程在这里获取对象和释放对象时是无锁的。

申请内存:

  1. 当内存申请size<=256KB,先获取到线程本地存储的thread cache对象,计算size映射的哈希桶自由链表下标i。
  2. 如果自由链表_freeLists[i]中有对象,则直接Pop一个内存对象返回。
  3. 如果_freeLists[i]中没有对象时,则批量从central cache中获取一定数量的对象,插入到自由链表并返回一个对象。

释放内存:

4. 当释放内存小于256k时将内存释放回thread cache,计算size映射自由链表桶位置i,将对象Push到_freeLists[i]。

5. 当链表的长度过长,则回收一部分内存对象到central cache。

如何保证线程可以创建属于自己的thread cache?

线程局部存储(TLS),是一种变量的存储方法,这个变量在它所在的线程内是全局可访问的,但是不能被其他线程访问到,这样就保持了数据的线程独立性。而熟知的全局变量,是所有线程都可以访问的,这样就不可避免需要锁来控制,增加了控制成本和代码复杂度。

thread cache代码框架:

#pragma once
#include "Common.h"
class ThreadCache
{
public:
// 申请和释放内存对象
void* Allocate(size_t size);
void Deallocate(void* ptr, size_t size);
// 从中心缓存获取对象
void* FetchFromCentralCache(size_t index, size_t size);
// 释放对象时,链表过长时,回收内存回到中心缓存
void ListTooLong(FreeList& list, size_t size);
private:
FreeList _freeLists[NFREELIST];
};
// TLS thread local storage(线程本地存储,每个线程都有自己的线程本地存储)
//有了TLS,线程来访问就不需要加锁了,被static修饰,只在当前文件可见
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
// 管理切分好的小对象的自由链表
class FreeList
{
public:
void Push(void* obj)
{
assert(obj);
// 头插
//*(void**)obj = _freeList; //*(void**)obj取obj头上4个或8个字节指向_freeList
NextObj(obj) = _freeList;
_freeList = obj;
++_size;
}
void PushRange(void* start, void* end, size_t n)
{
NextObj(end) = _freeList;
_freeList = start;
// 测试验证+条件断点
/*int i = 0;
void* cur = start;
while (cur)
{
cur = NextObj(cur);
++i;
}
if (n != i)
{
int x = 0;
}*/
_size += n;
}
void PopRange(void*& start, void*& end, size_t n)
{
assert(n >= _size);
start = _freeList;
end = start;
for (size_t i = 0; i < n - 1; ++i)
{
end = NextObj(end);
}
_freeList = NextObj(end);
NextObj(end) = nullptr;
_size -= n;
}
void* Pop()
{
assert(_freeList);
// 头删
void* obj = _freeList;
_freeList = NextObj(obj);
--_size;
return obj;
}
bool Empty()
{
return _freeList == nullptr;
}
size_t& MaxSize()
{
return _maxSize;
}
size_t Size()
{
return _size;
}
private:
void* _freeList = nullptr;
size_t _maxSize = 1;
size_t _size = 0;
};

自由链表的哈希桶跟对象大小的映射关系

// 计算对象大小的对齐映射规则
class SizeClass
{
public:
// 整体控制在最多10%左右的内碎片浪费
// [1,128] 8byte对齐 freelist[0,16)
//假设需要129字节,会分配给你144字节给你,就有15字节的浪费 15/144=0.104
// [128+1,1024] 16byte对齐 freelist[16,72)
//假设需要1025个字节,会分配给你1152字节给你,就有127字节的浪费 127/1152=0.11
// [1024+1,8*1024] 128byte对齐 freelist[72,128)
// [8*1024+1,64*1024] 1024byte对齐 freelist[128,184)
// [64*1024+1,256*1024] 8*1024byte对齐 freelist[184,208)
/*size_t _RoundUp(size_t size, size_t alignNum)
{
size_t alignSize;
if (size % alignNum != 0)
{
alignSize = (size / alignNum + 1)*alignNum;
}
else
{
alignSize = size;
}
return alignSize;
}*/
// 1-8
static inline size_t _RoundUp(size_t bytes, size_t alignNum)
{
return ((bytes + alignNum - 1) & ~(alignNum - 1));
}
static inline size_t RoundUp(size_t size)
{
if (size <= 128)
{
return _RoundUp(size, 8);
}
else if (size <= 1024)
{
return _RoundUp(size, 16);
}
else if (size <= 8*1024)
{
return _RoundUp(size, 128);
}
else if (size <= 64*1024)
{
return _RoundUp(size, 1024);
}
else if (size <= 256 1024 return _roundupsize 81024 else />256KB
{
return _RoundUp(size, 1<<PAGE_SHIFT);
}
}
static inline size_t _Index(size_t bytes, size_t align_shift)
{
return ((bytes + (1 << align_shift - 1>> align_shift) - 1;
}
// 计算映射的哪一个自由链表桶
static inline size_t Index(size_t bytes)
{
assert(bytes <= MAX_BYTES);
// 每个区间有多少个链
static int group_array[4] = { 16, 56, 56, 56 };
if (bytes <= 128){
return _Index(bytes, 3);
}
else if (bytes <= 1024){
return _Index(bytes - 128, 4) + group_array[0];
}
else if (bytes <= 8 * 1024){
return _Index(bytes - 1024, 7) + group_array[1] + group_array[0];
}
else if (bytes <= 64 * 1024){
return _Index(bytes - 8 * 1024, 10) + group_array[2] + group_array[1] + group_array[0];
}
else if (bytes <= 256 * 1024){
return _Index(bytes - 64 * 1024, 13) + group_array[3] + group_array[2] + group_array[1] + group_array[0];
}
else{
assert(false);
}
return -1;
}

相关推荐

Python钩子函数实现事件驱动系统(created钩子函数)

钩子函数(HookFunction)是现代软件开发中一个重要的设计模式,它允许开发者在特定事件发生时自动执行预定义的代码。在Python生态系统中,钩子函数广泛应用于框架开发、插件系统、事件处理和中...

Python函数(python函数题库及答案)

定义和基本内容def函数名(传入参数):函数体return返回值注意:参数、返回值如果不需要,可以省略。函数必须先定义后使用。参数之间使用逗号进行分割,传入的时候,按照顺序传入...

Python技能:Pathlib面向对象操作路径,比os.path更现代!

在Python编程中,文件和目录的操作是日常中不可或缺的一部分。虽然,这么久以来,钢铁老豆也还是习惯性地使用os、shutil模块的函数式API,这两个模块虽然功能强大,但在某些情况下还是显得笨重,不...

使用Python实现智能物流系统优化与路径规划

阅读文章前辛苦您点下“关注”,方便讨论和分享,为了回馈您的支持,我将每日更新优质内容。在现代物流系统中,优化运输路径和提高配送效率是至关重要的。本文将介绍如何使用Python实现智能物流系统的优化与路...

Python if 语句的系统化学习路径(python里的if语句案例)

以下是针对Pythonif语句的系统化学习路径,从零基础到灵活应用分为4个阶段,包含具体练习项目和避坑指南:一、基础认知阶段(1-2天)目标:理解条件判断的逻辑本质核心语法结构if条件:...

[Python] FastAPI基础:Path路径参数用法解析与实例

查询query参数(上一篇)路径path参数(本篇)请求体body参数(下一篇)请求头header参数本篇项目目录结构:1.路径参数路径参数是URL地址的一部分,是必填的。路径参...

Python小案例55- os模块执行文件路径

在Python中,我们可以使用os模块来执行文件路径操作。os模块提供了许多函数,用于处理文件和目录路径。获取当前工作目录(CurrentWorkingDirectory,CWD):使用os....

python:os.path - 常用路径操作模块

应该是所有程序都需要用到的路径操作,不废话,直接开始以下是常用总结,当你想做路径相关时,首先应该想到的是这个模块,并知道这个模块有哪些主要功能,获取、分割、拼接、判断、获取文件属性。1、路径获取2、路...

原来如此:Python居然有6种模块路径搜索方式

点赞、收藏、加关注,下次找我不迷路当我们使用import语句导入模块时,Python是怎么找到这些模块的呢?今天我就带大家深入了解Python的6种模块路径搜索方式。一、Python模块...

每天10分钟,python进阶(25)(python进阶视频)

首先明确学习目标,今天的目标是继续python中实例开发项目--飞机大战今天任务进行面向对象版的飞机大战开发--游戏代码整编目标:完善整串代码,提供完整游戏代码历时25天,首先要看成品,坚持才有收获i...

python 打地鼠小游戏(打地鼠python程序设计说明)

给大家分享一段AI自动生成的代码(在这个游戏中,玩家需要在有限时间内打中尽可能多的出现在地图上的地鼠),由于我现在用的这个电脑没有安装sublime或pycharm等工具,所以还没有测试,有兴趣的朋友...

python线程之十:线程 threading 最终总结

小伙伴们,到今天threading模块彻底讲完。现在全面总结threading模块1、threading模块有自己的方法详细点击【threading模块的方法】threading模块:较低级...

Python信号处理实战:使用signal模块响应系统事件

信号是操作系统用来通知进程发生了某个事件的一种异步通信方式。在Python中,标准库的signal模块提供了处理这些系统信号的机制。信号通常由外部事件触发,例如用户按下Ctrl+C、子进程终止或系统资...

Python多线程:让程序 “多线作战” 的秘密武器

一、什么是多线程?在日常生活中,我们可以一边听音乐一边浏览新闻,这就是“多任务处理”。在Python编程里,多线程同样允许程序同时执行多个任务,从而提升程序的执行效率和响应速度。不过,Python...

用python写游戏之200行代码写个数字华容道

今天来分析一个益智游戏,数字华容道。当初对这个游戏颇有印象还是在最强大脑节目上面,何猷君以几十秒就完成了这个游戏。前几天写2048的时候,又想起了这个游戏,想着来研究一下。游戏玩法用尽量少的步数,尽量...

取消回复欢迎 发表评论: