高并发内存池(三)Central Cache的框架及内存申请实现

目录

一、Central Cache的框架

二、Central Cache的实现

2.1CentralCache.h

2.2Span/SpanList

2.3CentarlCache.cpp


一、Central Cache的框架

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

 申请内存:

1. 当thread cache中没有内存时,就会批量向central cache申请一些内存对象,这里的批量获取对 象的数量使用了类似网络tcp协议拥塞控制的慢开始算法;central cache也有一个哈希映射的 spanlist,spanlist中挂着span,从span中取出对象给thread cache,这个过程是需要加锁的,不 过这里使用的是一个桶锁,尽可能提高效率。因为多个线程同时访问Central Cache时不一定访问的都是同一个桶,当访问的是同一个桶时再加锁,不然就不用加锁,互不影响。

2. central cache映射的spanlist中所有span的都没有内存以后,则需要向page cache申请一个新的 span对象,拿到span以后将span管理的内存按大小切好作为自由链表链接到一起。然后从span 中取对象给thread cache。

3. central cache的中挂的span中use_count记录分配了多少个对象出去,分配一个对象给thread cache,就++use_count。

二、Central Cache的实现

2.1CentralCache.h

在Central Cache中,我们也使用了单例模式,与Thread Cache中的TLS不同,Central Cache是整个程序全局性的中心内存池,负责直接调度多个线程的内存分配,处理多个线程的内存申请,所以采用单例模式,将构造函数私有化,禁止拷贝构造,禁止随意的去构造和创建对象。

而其中维护着一个和每个thread cache下标个数相同的哈希桶,不过每个桶中挂着的是每个结点为Span*的SpanList,是一个双向循环链表。每个桶中Span的大小是从0-207(共208)个依次递增的。当有线程来向Central Cache申请内存时,根据申请size的大小去对应的桶中,找到还有内存的span,将其切成thread cache所需要的大小然后用链表串起来,根据具体情况将对应的一段链表再头插到具体某个thread cache对应的哈希桶中。

static const size_t NFREELIST = 208;
//这里我们采用创建单例模式,因为CentralCache是全局属性的,每个进程只有一个,
//所以多线程并发时也都是访问这一个CentralCache
class CentralCache
{
public:
	static CentralCache* GetInstance()
	{
		return &_sInst;
	}

	//获取一个非空的Span
	Span* GetOneSpan(SpanList& list, size_t byte_size);

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

private:
	SpanList _spanList[NFREELIST];//和thread cache相对应,有相同的桶的个数

private:
	CentralCache()//构造函数私有化,禁止随意构造
	{}
	CentralCache(const CentralCache&) = delete;//禁止拷贝构造
	//const CentralCache operator = (const CentralCache&) = delete;

	static CentralCache _sInst;//构建单例
};

2.2Span/SpanList

在博主对项目研究实现期间,Span的设计也是让博主叹为观止,Span的设计不仅可以用来管理Central Cache中的数据,和向下兼容thread cache的_freelist结构,还可以用于PageCache的实现及设计,一箭三雕,一石三鸟,谷歌前辈工程师在tcmalloc的实现中,既做到了将整个内存池分层管理,相互解耦,又通过Span这一自定义数据类型将三层相互关联,统一调度。可谓妙哉!

整个高并发内存池的设计原理是先向堆空间申请一大块连续的内存,然后通过三层将其最终切成需要用的字节大小,然后统一管理分配。在某个线程退出或释放内存时,对零碎的内存碎片也更好的做处理和回收。

static const size_t MAX_BYTES = 256 * 1024;
static const size_t NFREELIST = 208;

static const size_t PAGE_SHIFT = 13;//一页=8kb=2^13字节
//因为WIN 64中也包含了WIN 32所以 将WIN64放在前面,如果当前机器是64位,就不会走#elif了
#ifdef _WIN64 
	 typedef unsigned long long PAGE_ID;
#elif _WIN32
	typedef size_t PAGE_ID;
#else
	//error
#endif

PAGE_ID  _pageId是根据当前机器是x64还是x32来决定的,前者地址是8字节,后者地址是4字节,而PAGE_ID则代表的就是当前Span起始地址的页号,当我们用其来对指针变量做强转时它就可以根据当前机器位数来往后读取对应的字节数,从而完整的读到整个指针,方便之后将地址转换为页号。

我们可以将页理解为一种存储单位,在高并发内存池中,我们将一页的大小定为8KB,也就是8*1024 Byte,而页号就是将整个内存抽象为按8KB作为一页后根据其实际地址/8KB后得出的数字,这里也体现出了分层管理,thread cahce按字节给用户的需求分配空间,而Central Cache则是根据thread Cache的需求将Page Cache分给它的页进行切分连接,而Page则是按页为单位对大块内存进行切分。

size_t _n 是记录当前Span包含了几页,在整个_spanList中,不同的桶下挂的span大小不同,比如8byte对应的0号桶中的span,这个span中的页数就为1,1页=8kb,8kb可以切出1024个8byte大小,一个span中有一页,绰绰有余。

_next _prev这无需多言,双向循环链表,每个节点需要两个指针一个指向前,一个指向后。

size_t  _useCount记录当前Span中切出来了多少个小块内存,每次将其分给thread时就--,thread用完将内存返回后就++,当再次加到初始化时的个数时说明小内存块全回来了,这时就可以将整个Span再返还给Page Cache,再由Page Cache统一调度分配。

void* _freeList 切好的小块内存的自由链表,切好的小内存块全部头插到_freeList中,便于向thread cache分配和回收。

static const size_t NFREELIST = 208;
#ifdef _WIN64 
	 typedef unsigned long long PAGE_ID;
#elif _WIN32
	typedef size_t PAGE_ID;
#else
	//error
#endif
struct Span//Central Cache 和 Page Cache共用的,Central中_freelist中有切好的小块内存
{

	PAGE_ID _pageId = 0;//大块内存起始页的页号
	size_t _n = 0;//页的数量

	Span* _next = nullptr;//双向链表结构
	Span* _prev = nullptr;

	size_t _useCount = 0;//切好小块内存,被分配给thread cache的计数,如果=0说明该Span下的内存块全收回来了没有被分配给线程,或者下面没有内存
	void* _freeList = nullptr;//切好的小块内存的自由链表

};

2.3CentarlCache.cpp

这里也是分两部分,因为Central Cache属于中间层,起到一个承上启下的枢纽作用,所以分为:

size_t CentralCache::FecthRangObj(void*& start, void*& end, size_t batchNum, size_t size),从中心缓存获取一定数量的内存给thread cache。

Span* CentralCache::GetOneSpan(SpanList& list, size_t size)获取一个非空的Span,如果_spanlist中有符合要求的,就在对应的桶下找到有内存的span取一部分出来,如果没有就去上层找PageCache申请内存。

注意,当Central Cache收到Thread Cache的内存申请但是Central Cache中也没有内存时,就需要继续往上找Page Cache去申请内存,此时根据Thread Cache所申请的size,来决定找Page Cache要几页(一页8kb),此时Page Cache分配给Central Cache的Span里所存的页数size_t _n是一大块连续的空间,所以申请完毕后需要Central自己对齐进行切分处理,将其切成size大小的内存块,然后根据Thread Cache的需要返回相应个数的内存块,剩下多余的则挂到对应的_spanList[index]中的对应的某个Span中,等待其他线程下次申请。

//Common.h    
//当thread cache某个哈希桶内部为空时,就需要去找CentralCache获取内存
	//这时如果直接给太多会导致出现大量内存碎片
	//如果给太少如果用到此桶的次数比较多就会频繁的申请锁去找CentralCache,降低了效率
	//所以此算法可以很好的根据当前申请获取哈希桶的量级来计算出给线程返回多少个元素

	//一次thread从中心缓存获取多少个
	static size_t NumMoveSize(size_t size)
	{
		assert(size > 0);
		size_t num = MAX_BYTES / size;
		if (num > 512) num = 512;
		if (num < 2) num = 2;

		return num;
	}
	//计算一次向系统获取几个页
	static size_t NumMovePage(size_t size)
	{
		size_t num = NumMoveSize(size);
        //计算该大小的数据一般在central中能获取几个给线程,
        //根据获取的多少来判断要切几个页给central cache
		size_t npage = num * size;
        //因为这里是给central而不是直接给thread,所以一次会多给一                                    
        //些,然后再交由central去管理

		npage >>= PAGE_SHIFT;
        //一页是8kb,也就是2的13次方,所以此时拿字节数除8kb就知道该切多少页
		if (npage == 0)
        //如果size过小比如申请的是8字节,那最后除出来的npage才0.5,此时直接给1页
			npage = 1;

		return npage;
	}
//Common.h:
static void*& NextObj(void* obj)
{
	return *(void**)obj; 
}


//Centralcache.cpp
CentralCache CentralCache::_sInst;

//获取一个非空的Span
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
	//查看当前的spanlist中是否有未分配的span
	Span* it = list.Begin();//begin是head头节点指向的下一个
	while (it != list.End())//list是一个双向循环链表,end就回到了head
	{
		if (it->_freeList != nullptr)
		{
			return it;
		}
		else
		{
			it = it->_next;
		}
	}
	
	//走到这里说明所要桶中没有空闲的Span中的freelist有元素,或者没有Span,只能去找Page cache要

	//先将桶锁解掉,这样如果于此同时其他线程释放相应的内存回来,不会阻塞
	list._mtx.unlock();

	//因为Page cache之间的桶都是相互访问更改的 ,所以要加一个全局的锁
	PageCache::GetInstance()->_pageMtx.lock();
	Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
	PageCache::GetInstance()->_pageMtx.unlock();

	//拿到以后直接对拿到的span进行切分,因为此时只有当前进程获取到了新span,不需要加锁,其他线程访问不到

	//计算span大块内存的起始地址和大块内存的大小(字节数)
	char* start = (char*)(span->_pageId << PAGE_SHIFT);//为什么要用char* 因为char刚好一个字节,每次加一都往后移一个,方便之后切分span
	size_t bytes = span->_n << PAGE_SHIFT;
	char* end = start + bytes;//计算出区间

	//开始将span切成我们要的大小然后链接起来
	span->_freeList = start;
	start += size;
	void* tail = span->_freeList;
	int i = 1;//i负责记录切了多少块
	//让start先在前面走将内存切成一个个大小为size的内存卡,
	//tail负责取出刚刚start切除的内存的前4/8个字节作为指针保存当前start所在位置的起始地址
	while (start < end)//如果刚好等于end说明刚好切完,如果超过start说明后面还有一点内存没用
	{
		++i;
		NextObj(tail) = start;
		tail = NextObj(tail);
		start += size;
	}

	//将span切好以后,需要将span挂到桶里的时候,这时候再加锁
	list._mtx.lock();
	list.PushFront(span);

	return span;//将切好的span还给central cache
}

//从中心缓存获取一定数量的内存给thread cache
size_t CentralCache::FecthRangObj(void*& start, void*& end, size_t batchNum, size_t size)
{
	size_t index = SizeClass::Index(size);
	_spanList[index]._mtx.lock();
	Span* span = GetOneSpan(_spanList[index], size);
	assert(span);
	assert(span->_freeList);

	//从span中获取bathNum个对象
	//如果不够,有多少拿多少

	start = span->_freeList;
	end = start;
	size_t i = 0;
	size_t actualNum = 1;//此时走到这里span中的freelist至少有一个元素,而start已经指向它,所以已经有一个元素了
	while (i<batchNum-1&&NextObj(end)!=nullptr)
	{
		end = NextObj(end);
		++i;
		++actualNum;
	}

	span->_freeList = NextObj(end);
	NextObj(end) = nullptr;
	span->_useCount += actualNum;

	_spanList[index]._mtx.unlock();

	return actualNum;
}
  • 40
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

C+五条

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值