C++实现高并发内存池

发布时间:2022-07-05 发布网站:脚本宝典
脚本宝典收集整理的这篇文章主要介绍了C++实现高并发内存池脚本宝典觉得挺不错的,现在分享给大家,也给大家做个参考。

高并发内存池

  • 1. 需求分析
    • 1.1 直接使用new/delete、malloc/free存在的问题
    • 1.2 定长内存池的优点和缺点
    • 1.3高并发内存池要解决的问题
  • 2. 总体设计思路
  • 3. 申请流程
  • 4. 释放流程
  • 5. 细节剖析
    • 5.1 ThreadCache
    • 5.2 CentralCache
    • 5.3 PageCache
    • 5.4 PageMap
  • 6. 性能测试
  • 7. 项目不足及扩展学习
  • 8. GITee原码链接
  • 9. 扩展补充和引用blog链接

1. 需求分析

池化技是计算机中的一种设计模式,内存池是常见的池化技术之一,它能够有效的提高内存的申请和释放效率以及内存碎片等问题,但是传统的内存池也存在一定的缺陷,高并发内存池相对于普通的内存池它有自己的独特之处,解决了传统内存池存在的一些问题。

1.1 直接使用new/delete、malloc/free存在的问题

new/delete用于c++中动态内存管理而malloc/free在c++和c中都可以使用,本质上new/delete底层封装了malloc/free。无论是上面的那种内存管理方式,都存在以下两个问题F1a;

  • 效率问题:频繁的在堆上申请和释放内存必然需要大量时间,降低了程序的运行效率。对于一个需要频繁申请和释放内存的程序来说,频繁调用new/malloc申请内存,delete/free释放内存都需要花费系统时间,频繁的调用必然会降低程序的运行效率。
  • 内存碎片:经常申请小块内存,会将物理内存“切”得很碎,导致内存碎片。申请内存的顺序并不是释放内存的顺序,因此频繁申请小块内存必然会导致内存碎片,造成“有内存但是申请不到大块内存”的现象。

1.2 定长内存池的优点和缺点

针对直接使用new/delete、malloc/free存在的问题,定长内存池的设计思路是:预先开辟一块大内存,程序需要内存时直接从该大块内存中“拿”一块,提高申请和释放内存的效率,同时直接分配大块内存还减少了内存碎片问题。

  • 优点简单粗暴,分配和释放的效率高,解决实际中特定场景下的问题有效。
  • 缺点:功能单一,只能解决定长的内存需求,另外占着内存没有释放。

对于STL中的空间配置器就是采用的这种方式,当申请小于128字节的内存就是用定长内存池,当超过时,就直接使用Malloc和free

C++实现高并发内存池

定长内存时详解链接:https://bLOG.csdn.net/MEANSWER/article/details/118343707

1.3高并发内存池要解决的问题

基于以上原因,设计高并发内存池需要解决以下三个问题:

  • 效率问题
  • 内存碎片问题
  • 多线程并发场景下的内存释放和申请的锁竞争问题

2. 总体设计思路

高并发内存池整体框架由以下三部分组成,各部分的功能如下:

  • 线程缓存(ThreadCache)每个线程独有线程缓存,主要解决多线程下高并发运行场景线程之间的锁竞争问题。线程缓存模块可以为线程提供小于64k内存的分配,并且多个线程并发运行不需要加锁。线程从这里申请内存不需要加锁,每个线程独享一个ThreadCache,这也就是这个并发内存池高效的地方(这就是tcmalloc名字的本质来,在这里具体的实现采用的是TLS(thread local storage 本地线程存储,可以理解为每个线程独有的全局变量,但是是本地的)))。(本质上ThreadCache里面就是由hash映射的定长的内存桶
  • 中心缓存(CentralCache):中心缓存是所有线程所共享,thread cache是按需从central cache中获取的对象。central cache周期性的回收thread cache中的对象,避免一个线程占用了太多的内存,而其他线程的内存紧。达到内存分配在多个线程中更均衡的按需调度的目的。central cache是存在竞争的,所以从这里取内存对象是需要加锁,(比如说一个线程Thread Cache下的8字节映射的自由链表过长,就需要还回Central Cache,但是此时另外的一个线程Thread Cache下8字节的自由链表没有了,就需要向Central Cache要,此时就需要加锁,因为这种情况很少会出现)不过一般情况下在这里取内存对象的效率非常高,所以这里竞争不会很激烈。
  • 页缓存(PageCache):以页为单位申请内存,为中心控制缓存提供大块内存。当中心控制缓存中没有内存对象时,可以从page cache中以页为单位按需获取大块内存,同时page cache还会回收central control cache的内存进行合并缓解内存碎片问题。

C++实现高并发内存池

注:怎么实现每个线程都拥有自己唯一的线程缓存呢?

  • TLS分为静态的和动态的: 静态详解链接:https://blog.csdn.net/evilswords/article/details/8191230 动态详解链接:https://blog.csdn.net/yusiguyuan/article/details/22938671

3. 申请流程

我们的并发内存池项目对外只暴露两个接口,对于申请的接口就是ConcurrentAlloc(),如果申请的内存大于64KB,直接走的就是PageCache所提供的的NewSpan(),但是走这个也有可能有两种情况,一种是介于16页——128页之间,一开始就会直接的申请上来一块128页的内存然后进行切分,返回你需要的那一部分。大于128页直接调用VirtualAlloc进行向系统申请内存。如果是小于64KB,那么就会走ThreadCache所提供的Allocate()接口,计算出要找哪一个索引下标的freelist,如果有就直接返回,没有则会调会FetchFromCentralCache(),通过你要的内存size计算出需要给你返回的批量个数(慢启动方式)以及实际上真正能给你返回的数量调用CentralCache的FetchRangeObj(),但是有可能CentralCache中的SpanList[i]下没有Span或者内存都被用完了,所以首先就是得到块有内存的Span,调用GetOneSpan()接口,如果该Span中的list不为nullptr说明还有内存,如果为空就需要向PageCache要一块Span,调用NewSpan()接口。计算索引看PageCache下是否有合适的页,如果没有则需要向后找,在没有就只能向系统直接申请一块128页的内存,然后进行切分了。由于CentralCache中的Span都是切好的,所以在得到这个新的页的时候,也应该按照对应的内存大小将他切分好然后在返回CentralCache

4. 释放流程

一块块内存还回来挂接在ThreadCache中对应的FreeList中,当其中一个FreeList挂接的太长的时候就需要进行归还给CentralCache(这里选择归还的条件就是自由链表中的内存个数Size大于MaxSize),从该FreeList中取出MaxSize个内存归还到CentralCache,但是每一块小内存都可能来自于不同的Span(根据每一块的小内存的起始地址算出它所对应的页号,然后还有一个map可以通过页号找到所对应的Span,那么就可以确保每一块小内存都归还给当初所切出来的Span中),在CentralCache中的每一个大块Span里面有一个usecount,如果为0的时候,说明分给ThreadCache的内存就都还回来了,那么为了能够合成更大的页,就需要再把该Span还回PageCache中,每一个大块的Span里面都有一个PageiD(页号)和页数,那么就可以进行在PageCache中进行前后的搜索,找到是否还有大块的Span没有使用然后进行合并,成为更大的页。

5. 细节剖析

5.1 ThreadCache

ThreadCache的主要功能就是为每一个线程提供64K以下大小内存的申请。为了方便管理,需要提供一种特定的管理模式,来保存未分配的内存以及被释放回来的内存,以方便内存的二次利用。这里的管理通常采用将不同大小的内存映射在哈希表中,链接起来。而内存分配的最小单位是字节,64k = 102464Byte如果按照一个字节一个字节的管理方式进行管理,至少也得需要102464大小的哈希表对不同大小的内存进行映射。为了减少哈希表长度,这里采用不同段的内存使用不同的内存对齐规则,将浪费率保持在1%~12%之间。具体结构如下:

C++实现高并发内存池

具体说明如下

  • 为了将内存碎片浪费保持在12%左右,这里使用不同的对齐数进行对齐。
  • 0 ~ 128采用8字节对齐,129 ~ 1024采用16字节对齐,1025 ~ 8 * 1024采用128字节对齐, 8 * 1024~64*1024采用1024字节对齐;内存碎片浪费率分别为:7/8,15/144,127/1152,1023/8 * 1024 + 1024均在12%左右(除了第一块按照8字节对齐的浪费率)。同时,8字节对齐时需要[0,15]共16个哈希映射;16字节对齐需要[16,71]共56个哈希映射;128字节对齐需要[72,127]共56个哈希映射;1024字节对齐需要[128,184]共56个哈希映射。
	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 <= 8192){
			return _Index(bytes - 1024, 7) + group_array[1] + group_array[0];
		}
		else if (bytes <= 65536){
			return _Index(bytes - 8192, 10) + group_array[2] + group_array[1] + group_array[0];
		}

		assert(false);

		return -1;
	}

C++实现高并发内存池

ThreadCache.h

#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&amp; list, size_t size);
private:
	FreeList _freeLists[NFREELISTS];
};

static __declsPEc(thread) ThreadCache* tls_threadcache = nullptr;

申请内存

  • 当内存申请size<=64k时在ThreadCache中申请内存,计算size在自由链表中的位置,如果自由链表中有内存时,直接从FistList[i]中Pop一下对象,@R_116_1304@是O(1),且没有锁竞争。
  • 当FreeList[i]中没有对象时,则批量从CentralCache中获取一定数量的内存,返回一个内存并将剩余的批量申请上来的内存插入到对应的自由链表中。

释放内存

  • 当释放内存小于64k时将内存释放回ThreadCache,计算size在自由链表中的位置,将对象Push到FreeList[i].
  • 当链表的长度过长,则回收一部分内存到CentralCache。

5.2 CentralCache

  • CentralCache本质是哈希映射的SpanList对象构建的数组,而SpanList使用的是双向带头循环链表,为的就是能够更好的插入和删除效率更高
  • 每一个SpanList后面所挂接的Span,都是为了ThreadCache中特定大小内存服务的
  • 这里需要注意的是,每一个线程都有一个Threadcache,但是Centralcache只有一个,主要就是起到承上启下的作用,并且需要加锁,但是力度不需要那么大,加的是桶锁(当一个线程访问的是CentralCache中8字节映射的地方,那么另一个线程是可以访问16字节内的Span的,也就一定程度提高了效率)
  • 为了保证全局只有唯一的CentralCache,这个类被可以设计成了单例模式

Centralcache作为Threadcache和Pagecache的桥梁,起到承上启下的作用。它需要向Threadcache提供切割好的小块内存,同时他还需要回收Threadcache中的过多内存,在分配给其他其他Threadcache使用,起到资源调度的作用。如果Centralcache中的span已经完全由Threadcache归还回来还需要向下层交付,以便合成更大的页,解决内存碎片的问题它的结构如下:

C++实现高并发内存池

CentralCache.h

#pragma once
#include "Common.h"

//CentralCache是要加锁的,但是锁的力度不需要太大,只需要加一个桶锁,因为只有多个线程同时取一个Span
//要保证CentralCache和PageCache对象都是全局唯一的,所以直接使用单例模式
//且这里使用的是饿汉模式---一开始就进行创建(main函数之前就进行了创建)
class CentralCache
{
public:
	//返回指针挥着引用都是可以的
	static CentralCache* GetInstance()
	{
		return &_inst;
	}


	// 从中心缓存获取一定数量的对象给thread cache
	size_t FetchRangeObj(void*& start, void*& end, size_t n, size_t byte_size);

	// 从SpanList或者page cache获取一个span
	Span* GetOneSpan(SpanList& list, size_t byte_size);


	// 将一定数量的对象释放到span跨度
	void ReleaseListToSpans(void* start, size_t byte_size);
private:
	SpanList _spanLists[NFREELISTS]; // 按对齐方式映射    这里的span被切过了,并且有一部分小对象已经切分出去了

private:

	CentralCache() = default;
	CentralCache(const CentralCache&) = delete;
	CentralCache& operator=(const CentralCache&) = delete;

	static CentralCache _inst;
};

申请内存

  • 一开始当ThreadCache中FreeList下没有挂接内存时,就会批量向CentralCache申请一些内存(由于就近原则,在申请了某一大小的内存之后,我们有理由他还会再次申请,但是如果一次就只返回一个,那么下次来还需要再次进入CentralCache中,而这个是会加锁的,势必会影响申请的效率,所以就一次返回“批量”内存)
  • 但是如果这个“批量”是一个固定的大小,那么万一他就只是要这一次,那么返回给ThreadCache的多余内存势必就造成了浪费,所以这里采用了一种慢启动的方式进行返回,一开始就返回一个,随着你要的次数的增加,他也就会逐渐给你返回更多
  • 但是当进入CentralCache中发现对应的SpanList中的Span也没有足够的内存时,就需要向PageCache申请一块Span,根据你要的内存大小,来考虑给你返回的页的大小(比如说你只需要8字节的内存,那么返回一页就足够了,因为已经可以切割出来很多块了,但是当你是要一个64K的内存时,就给你返回2页,这样来进行控制)
  • 从PageCache中返回来的大块Span是完整,但是在CentralCache中的Span都是已经被切割好并且被分出去一部分的Span,在Span内有一个freelist,用来管理已经切割好的小块内存,如果分配一块小内存给ThreadCache,就++_usecount。

释放内存

  • 当ThreadCache中的某一个FreeList下的内存挂接过长或者线程销毁,则会将内存释放回CentralCache中的,但是由于内存归还的时间顺序等都是不知道的,所以每一个归还回来的小块内存是不知道属于CentralCache中SpanList中的哪一个Span的,所以开始的时候采用的是使用map<PageID,Span*>构建一个映射,通过还回来的地址右移PAGE_SHIFT(页的大小),就可以算出属于哪一个页,找到该SpanList中对应的Span(这个地方是要修改的,最终设计不是这样,要改为基数树)
  • 当_usecount减到0时则表示所有对象都回到了span,则将Span需要释放回PageCache,为的是能够合出更大的页,减少内存碎片问题。PageCache中会对前后搜索空闲页进行合并。

5.3 PageCache

Pagecache是以页为单位进行内存管理的,它是将不同页数的内存利用哈希进行映射,最多映射128页内存,具体结构如下:

C++实现高并发内存池

PageCache.h

#pragma once
#include "Common.h"
#include "PageMap.h"

class PageCache
{
public:
	static PageCache* GetInstance()
	{
		return &_sInst;
	}
	// 向系统申请k页内存挂到自由链表
	void* SystemAllocPage(size_t k);

	Span* NewSpan(size_t k);

	//获取从对象到span的映射
	Span* MapObjectToSpan(void* obj);

	void ReleaseSpanToPageCache(Span* span);
private:
	SpanList _spanList[NPAGES];	// 按页数映射

	//std::map<PageID, Span*> _idSpanMap; //这里是有可能多个页都映射同一个Span的
	//tcmalloc 基数树 效率更高

	TCMalloc_PageMap2<32-PAGE_SHIFT> _idSpanMap;



	std::recursive_mutex _mtx;
private:
	PageCache() = default;
	PageCache(const PageCache&) = delete;
	PageCache& operator=(const PageCache&) = delete;

	static PageCache _sInst;
};

申请流程:

  • 对于申请介于16页和128页之间内存,也会走PageCache中提供的一个接口NewSpan(),会把这块内存依旧封装成一个Span,为的就是还回来的时候能够直接将其挂接在PageCache中,那么下一次申请大块内存就不需要向系统申请了,也算提高了效率
  • 正常的申请内存流程,ThreadCache没有了,向CacheCache要,CentralCache中也没有合适的Span的时候,就需要向PageCache要,根据单个内存Size的大小计算出你需要的页数,然后就去PageCache中对应的SpanList中找是否有合适的,如果没有那就向后继续找,如果找到了就把这个更大的页切割为我需要的页数和剩余的页数挂接在对应的SpanList中,如果PageCache中后面也没有更大的,此时就直接向系统申请一块128页的内存,然后进行切割(这里代码实现使用的是子函数调用,相当于递归了,所以在这里面加锁的时候使用的是STL中的recursive_mutex锁)

释放流程:

  • 当是介于16页和128页内存的,释放回来的直接归还给PageCache中对应的SpanList
  • 如果是CentralCache中还回来的Span,首先就是对该Span进行一个清理工作,将他里面的freelist置为nullptr,因为PageCache中的Span都是大块且没有别切割过的,还要检查前后存在没有使用的页,如果有就继续进行合并,为的是能够合出更大的页,减少内存碎片的问题(但是这里还有一点就是,如果已经128页了,也就没有必要在继续进行合并了,因为PageCache中也没有能挂下该大块内存的位置)

Page Cache向系统申请内存时,前边我们说过每次直接申请128页的内存。这里需要说明的是,我们的项目中不能出现任和STL中的数据结构和库函数,因此这里申请内存直接采用系统调用VirtualAlloc。下面对VirtualAlloc详细解释: 函数声明如下

LpvOID VirtualAlloc{

LPVOID lpAddress, // 要分配的内存区域的地址 DWORD dwSize, // 分配的大小 DWORD flAllocationType, // 分配的类型 DWORD flProtect // 该内存的初始保护属性 };

参数说明:

  • LPVOID lpAddress, 分配内存区域的地址。当你使用VirtualAlloc来提交一块以前保留的内存块的时候,lpAddress参数可以用来识别以前保留的内存块。如果这个参数是NULL,系统将会决定分配内存区域的位置,并且按64-KB向上取整(roundup)。
  • SIZE_T dwSize, 要分配或者保留的区域的大小。这个参数以字节为单位,而不是页,系统会根据这个大小一直分配到下页的边界DWORD
  • flAllocationType, 分配类型 ,你可以指定或者合并以下标志:;mEM_COMMIT,MEM_RESERVE和MEM_TOP_DOWN。
  • DWORD flProtect 指定了被分配区域的访问保护方式

VirtualAlloc详解链接:https://baike.baidu.COM/item/VirtualAlloc/1606859?fr=aladdin

5.4 PageMap

  • 首先对于PageCache中所提供的一个MapObjectSpan()接口来说,这是一个对外暴露的接口,别人是可以任意的查看,并且不需要进入到PageCache中,但是保不准在高并发的场景下有另外一个线程将其对应关系进行了修改,所以这个接口是要加锁的。加完锁以后就发现,本来性能还不错,一下子变得很差。然后通过VS自带的性能分析工具进行检查发现这个接口由于频繁调用,但是还加了锁导致的。然后通过看tcmalloc的原码,发现人家在设计这一块的时候是没有加锁的,使用的是一个基数树结构
  • 其实可以把基数树看成多阶哈希的结构,对于32位平台下,最多可以申请到100多万页的内存,但是由于虚拟地址空间的分布,排除掉代码段,初始化未初始化等空间以及最上层的内核区,其实真正能申请到的内存也就是堆那一块的内存,所以如果直接使用哈希的直接定制法,那么势必会导致有大量的最前面和最后面所映射的空间造成浪费,所以对于通过Object找到Span采用了一个两层的基数树。第一层开了32的空间(里面不存数据,存的是指向下一层的指针),那么相当于每个空间就有2的15次方页要在这一块进行映射,如果没有使用到这2的15次方内的页,则就不会开底下的这层空间,如果使用了,则开底下的这层空间,然后找到相对应的位置存上所对应的Span(其实就是取了这块内存地址的后20位,15-19位用在找第一层的位置,0-14位用来找所对应底下的页映射的位置),这里就是想要通过页号找到对应的Span
  • 我们还想要通过页号找到对应的该Span中freelist上挂接内存的大小,目的就是为了能够在调用ConcurrentFree的时候能够直接传释放内存的地址,不需要再传他的大小,通过分析可以发现,在高并发的场景下,首先给PageCache加锁,然后一个线程从PageCache中申请了一块Span,需要将他切好,在返回给CentralCache,那么此时是不可能有线程能够访问到这块Span中的地址的,因为他还在PageCache,并且还没有切好呢,怎么可能访问到这里面的内存。当一个线程来读这块Span中的freelist下挂接内存的大小时,是不可能有线程能够对他已经切分好的size进行修改的。针对这个使用了一个一层的基数树,目的就是能够通过Span获得Size

基数树详解链接:https://blog.csdn.net/weixin_36145588/article/details/78365480

6. 性能测试

void benchmarkMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
	//创建nworks个线程
	std::vector<std::thread> vthread(nworks);
	size_t malloc_costtime = 0;
	size_t free_costtime = 0;
 
	//每个线程循环依次
	for (size_t k = 0; k < nworks; ++k)
	{
		//铺货k
		vthread[k] = std::thread([&, k]() {
			std::vector<void*> v;
			v.reserve(ntimes);
 
			//执行rounds轮次
			for (size_t j = 0; j < rounds; ++j)
			{
				size_t begin1 = clock();
				//每轮次执行ntimes次
				for (size_t i = 0; i < ntimes; i++)
				{
					v.push_back(malloc(16));
				}
				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 msn",
		nworks, rounds, ntimes, malloc_costtime);
 
	printf("%u个线程并发执行%u轮次,每轮次free %u次: 花费:%u msn",
		nworks, rounds, ntimes, free_costtime);
 
	printf("%u个线程并发malloc&free %u次,总计花费:%u msn",
		nworks, nworks*rounds*ntimes, malloc_costtime + free_costtime);
}
 
 
// 单轮次申请释放次数 线程数 轮次
void BenchmarkConcurrentMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
	std::vector<std::thread> vthread(nworks);
	size_t malloc_costtime = 0;
	size_t free_costtime = 0;
 
	for (size_t k = 0; k < nworks; ++k)
	{
		vthread[k] = std::thread([&]() {
			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(hcAlloc(16));
				}
				size_t end1 = clock();
 
				size_t begin2 = clock();
				for (size_t i = 0; i < ntimes; i++)
				{
					hCFree(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轮次,每轮次concurrent alloc %u次: 花费:%u msn",
		nworks, rounds, ntimes, malloc_costtime);
 
	printf("%u个线程并发执行%u轮次,每轮次concurrent dealloc %u次: 花费:%u msn",
		nworks, rounds, ntimes, free_costtime);
 
	printf("%u个线程并发concurrent alloc&dealloc %u次,总计花费:%u msn",
		nworks, nworks*rounds*ntimes, malloc_costtime + free_costtime);
}
 
int main()
{
	cout << "==========================================================" << endl;
	BenchmarkMalloc(100000, 4, 10);
	cout << endl << endl;
 
	BenchmarkConcurrentMalloc(100000, 4, 10);
	cout << "==========================================================" << endl;
 
	return 0;
}

该函数使用了C++11库里面的thread和lambda表达式。一共4个线程,每个线程申请释放内存10000次一共执行4轮来对比库里面的malloc和free

C++实现高并发内存池

在release版本下,我们所写的并发内存池项目申请和释放内存的确是要更好一些的

7. 项目不足及扩展学习

-项目的独立性不足

  1. 不足当前实现的项目中我们并没有完全脱离malloc,比如在内存池自身数据结构的管理中,如SpanList中的span等结构,我们还是使用的new Span这样的操作,new的底层使用的是malloc,所以没有完全脱离malloc。按理来说连thread、mutex等库里面的也不可以使用,因为你不确定是否他们的库里实现使用了new或者malloc申请内存如果此时threadCache销毁了,如果他还有内存怎么办?
  2. 解决方案项目中增加一个定长的ObjectPool的对象池,对象池的内存直接使用brk(Linux平台)、VirarulAlloc(Windows平台)等向系统申请,new Span替换成对象池申请内存。这样就完全脱离的malloc,替换掉malloc。(频繁申请的小块内存入span使用对象池申请空间,大块内存使用:VirtualAlloc)。针对上面的第三点会造成耽误内存还回CentralCache,导致小页无法合大,注册一个线程结束后回调清理ThreadCache的内存函数。③不同平台替换方式不同。 基于unix的系统上的glibc,使用了weak alias的方式替换。具体来说是因为这些入口函数都被定义成了weak symbols,再加上gcc支持 alias attribute,所以替换就变成了这种通用形式:void* malloc(size_t size) THROW attribute__ ((alias (tc_malloc))),因此所有malloc的调用都跳转到了tc_malloc的实现④有些平台不支持这样的东西,需要使用hook的钩子技术来做。
  • GCC attribute 之weak,alias属性(实际上就是当我们调用malloc的时候,他会自动跳转到我们自己实现的tc_malloc) https://blog.csdn.net/Bingoami/article/details/78683906
  • 有些平台不支持这样的东西,需要使用hook的钩子技术来做。详解链接: https://www.cnblogs.com/feng9exe/p/6015910.htML

8. Gitee原码链接

ConcurrentMemoryPool原码链接: https://gitee.com/meanswer/concurrent-memory-pool

9. 扩展补充和引用Blog链接

  • Linux进程分配内存的两种方式–brk() 和mmap()详解(这篇文章写的很好)链接:https://www.cnblogs.com/vinozly/p/5489138.html
  • 能不能把threadCache和CentralCache合并?角度:CentralCache的核心作用就是承上启下,CentralCache里面用的是桶锁,效率更高, a.CentralCache均衡多个线程之间的同一大小的内存需求 b. 他的Span都是至少有部分在用的,区分PageCache都是大块完整 c.它实现的是桶锁,因为一个span只会给一个桶用,不会再桶之间流动,效率更高,如果没有他的话,PageCache是一把大锁,因为PageCache中的span需要切小和合大,会再多个映射桶之间流动。
  • 几个内存池库的对比链接:https://blog.csdn.net/junlon2006/article/details/77854898
  • tcmalloc源码学习链接:https://www.cnblogs.com/persistentsnail/p/3442185.html
  • TCMALLOC 源码阅读链接:https://blog.csdn.net/math715/article/details/80654167
  • malloc原理详解链接:https://blog.csdn.net/hudazhe/article/details/79535220
  • malloc函数实现原理链接:https://blog.csdn.net/yeditaba/article/details/53443792
  • 参考C++实现高并发内存池Blog链接: link.
  • 链接: link.

脚本宝典总结

以上是脚本宝典为你收集整理的C++实现高并发内存池全部内容,希望文章能够帮你解决C++实现高并发内存池所遇到的问题。

如果觉得脚本宝典网站内容还不错,欢迎将脚本宝典推荐好友。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。