高并发内存池详解(C/C++/Windows/Linux)

目录

一、项目介绍

二、知识储备

三、项目效果

四、项目框架

 1、先模拟实现malloc

2、再模拟实现定长内存池

3、实现高并发内存池 

五、模拟实现malloc

1、编译环境

2、实现原理

2.1 malloc / free 简介

2.2 动态内存分配的系统调用:brk / sbrk

         2.3 实现malloc

六、定长内存池实现

6.1主要思想

6.2定长内存池特点

6.3运行结果 

 七、高并发内存池

 7.1thread_cache

线程缓存核心

申请内存:

释放内存:

7.2 central_cache

中心缓存核心

申请内存:

释放内存:

 7.3 page_cache

页缓存核心

申请内存:

释放内存:


 

一、项目介绍

该项目模拟实现简化版的tcmalloc,其中tcmalloc全称Thread-Caching Malloc,即线程缓存的malloc,实现了高效的多线程内存管理,用于替代系统的内存分配相关的函数(malloc、free)。

malloc的设计足够优秀,但是因为malloc是针对所有场景,那么其效率不会很高,而高并发内存池是专门针对多线程并发执行的场景,在这种场景下,高并发内存池的效率是要比malloc高的

该项目重在效率的提升(与malloc进行比较)以及解决内存碎片的问题

二、知识储备

C/C++、数据结构(链表、哈希桶)、操作系统内存管理、单例模式、多线程、互斥锁等

三、项目效果

 高并发内存池详解(C/C++/Windows/Linux)_第1张图片

可以看到四个线程并发执行,每个线程执行10轮次,每轮次调用内存分配器10000次,高并发内存池的效率将近是malloc的四倍

四、项目框架

 1、先模拟实现malloc

通过模拟实现malloc可以让我们知道malloc底层调用的系统函数是什么,以及最简单的管理内存池的方法

2、再模拟实现定长内存池

模拟实现定长内存池有如下两点原因:

        第一可以熟悉一下简单内存池是如何控制的

        第二它会作为后面高并发内存池的一个基础组件

3、实现高并发内存池 

 核心:

高并发内存池详解(C/C++/Windows/Linux)_第2张图片

实现的内存池需要考虑以下几方面的问题。
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对象,并且合并相邻的页,组成更大的页,缓解内存碎片
的问题

五、模拟实现malloc

1、编译环境

Linux/centos7、vim

2、实现原理

2.1 malloc / free 简介

void *malloc(size_t size)
void free(void *ptr)

malloc 分配指定大小的内存空间,返回一个指向该空间的指针。大小以字节为单位。返回 void* 指针,需要强制类型转换后才能引用其中的值。

free 释放一个由 malloc 所分配的内存空间。ptr 指向一个要释放内存的内存块,该指针应当是之前调用 malloc 的返回值。

 2.2 动态内存分配的系统调用:brk / sbrk

动态分配的内存都在堆中,堆从低地址向高地址增长:

 高并发内存池详解(C/C++/Windows/Linux)_第3张图片

使用brk或sbrk分配内存,将堆顶指针往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系) 

高并发内存池详解(C/C++/Windows/Linux)_第4张图片

 2.3实现malloc

 

 根据所要申请空间的大小使用sbrk从堆上分配空间,使用首次适应法进行分配:遍历整个链表(这里的链表是抽象的概念,实际是堆上连续的空间),找到第一个未被分配、大小合适的内存块;如果没有这样的内存块,则使用sbrk向操作系统申请扩展堆内存

这种方法的缺点是:
        已分配和未分配的内存块位于同一个链表中,每次分配都需要从头到尾遍历采用首次适应法,
        内存块会被整体分配,容易产生较多内部碎片

执行结果:

使用mymalloc预期的完成了内存的分配与使用 

六、定长内存池实现

 6.1主要思想

该内存池由以下三个成员变量维护:

高并发内存池详解(C/C++/Windows/Linux)_第5张图片

 分配内存时优先从_freeList中获取,其次从_memroy中获取内存

向系统申请与释放内存空间的函数不使用malloc\free,而是直接向堆申请页为单位的大块内存

Windows下使用系统调用VirtualAlloc,Linux下使用系统调用sbrk

高并发内存池详解(C/C++/Windows/Linux)_第6张图片

高并发内存池详解(C/C++/Windows/Linux)_第7张图片

6.2定长内存池特点

 1、固定大小的内存申请释放

 2、不考虑内存碎片、多线程执行等问题

6.3运行结果 

让系统的new与定长内存池在单线程下运行4轮次,每轮次执行100000

 

 七、高并发内存池

 7.1thread_cache

线程缓存核心

 thread cache是哈希桶结构,每个桶是一个按桶大小映射位置的内存块对象的自由链表。每个线程都会有一个thread cache对象,这样每个线程在这里获取对象和释放对象时是无锁的

 高并发内存池详解(C/C++/Windows/Linux)_第8张图片

线程缓存是每个线程独有的,那么这里使用线程本地存储(TLS)创建一个全局变量,该全局变量是每个线程独有的!,由该全局变量维护线程缓存。

 申请内存:

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

释放内存:

1. 当释放内存小于256k时将内存释放回thread cache,计算size映射自由链表桶位置i,将对象Push到_freeLists[i]。
2. 当链表的长度过长,则回收一部分内存对象到central cache,防止了某个线程浪费大量的内存空间。

7.2 central_cache

中心缓存核心

central cache也是一个哈希桶结构,他的哈希桶的映射关系跟thread cache是一样的。不同的是他的每个哈希桶位置挂是SpanList链表结构,不过每个映射桶下面的span中的大内存块被按映射关系切成了一个个小内存块对象挂在span的自由链表中

高并发内存池详解(C/C++/Windows/Linux)_第9张图片

 申请内存:

1. 当thread cache中没有内存时,就会批量向central cache申请一些内存对象,这里的批量获取对
象的数量使用慢开始算法;central cache也有一个哈希映射的spanlist,spanlist中挂着span,从span中取出对象给thread cache,这个过程是需要加锁的,不过这里使用的是一个桶锁,尽可能提高效率。
2. central cache映射的spanlist中所有span的都没有内存以后,则需要向page cache申请一个新的
span对象,拿到span以后将span管理的内存按大小切好作为自由链表链接到一起。然后从span
中取对象给thread cache。
3. central cache的中挂的span中use_count记录分配了多少个对象出去,分配一个对象给thread
cache,就++use_count

释放内存:

1. 当thread_cache过长或者线程销毁,则会将内存释放回central cache中的,释放回来时--
use_count。

2. 当use_count减到0时则表示所有对象都回到了span,则将span释放回page cache,
page cache中会对前后相邻的空闲页进行合并。 

 7.3 page_cache

页缓存核心

 高并发内存池详解(C/C++/Windows/Linux)_第10张图片

申请内存:

1. 当central cache向page cache申请内存时,page cache先检查对应位置有没有span,如果没有
则向更大页寻找一个span,如果找到则分裂成两个。比如:申请的是4页page,4页page后面没
有挂span,则向后面寻找更大的span,假设在10页page位置找到一个span,则将10页page
span分裂为一个4页page span和一个6页page span。
2. 如果找到_spanList[128]都没有合适的span,则向系统使用mmap、brk或者是VirtualAlloc等方式
申请128页page span挂在自由链表中,再重复1中的过程。
3. 需要注意的是central cache和page cache 的核心结构都是spanlist的哈希桶,但是他们是有本质
区别的,central cache中哈希桶,是按跟thread cache一样的大小对齐关系映射的,他的spanlist
中挂的span中的内存都被按映射关系切好链接成小块内存的自由链表。而page cache 中的
spanlist则是按下标桶号映射的,也就是说第i号桶中挂的span中的内存都是i页大小的内存

释放内存:

1. 如果central cache释放回一个span,则依次寻找span的前后page id的没有在使用的空闲span,
看是否可以合并

2. 如果合并继续向前寻找。这样就可以将切小的内存合并收缩成大的span,减少
内存碎片。 

 项目源码:GitHub - Codeguer/memorypool: 高并发内存池

你可能感兴趣的:(C++精华,c++,c语言,visualstudio,哈希算法,链表)