六、高并发内存池--Central Cache

六、高并发内存池–Central Cache

6.1 Central Cache的工作原理

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

六、高并发内存池--Central Cache_第1张图片
中心缓存central cache是如何工作的?

申请内存:

  1. 当thread cache中没有内存时,就会批量向central cache申请一些内存对象,这里的批量获取对象的数量使用了类似网络tcp协议拥塞控制的慢增长算法,具体细节参考代码实现;central cache也有一个哈希映射的spanlist,spanlist中挂着span,因为central cache在全局只有一个,所以从span中取出对象给thread cache,这个过程是需要加锁的,不过这里使用的是一个桶锁,而不是加一把大锁把整个central cache锁住,这是为什么呢?
    原因是central cache中哈希桶的映射关系和thread cache中哈希桶的映射关系是完全一致的,并且thread cache中哪个哈希桶的自由链表中没有内存对象,就会去central cache中相同位置的哈希桶中获取对象,不会到别的哈希桶获取对象的,因为别的哈希桶中span的自由链表的对象的大小是不一样的,所以我们只需要加上桶锁锁住当前位置的spanList即可,其它线程到central cache的其它位置的span中获取对象和我是不会相互影响的,所以加桶锁而不加大锁能够大大提高thread cache向central cache申请内存的效率。并且大多数情况线程在thread cache就能申请到内存,所以到central cache中申请内存对象的概率不会很高,因此桶锁的竞争也不会特别激烈,所以使用桶锁能尽可能提高效率。

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

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

释放内存:
当thread cache中的自由链表过长或者线程销毁,则会将内存释放回central cache中的,释放回来时–_useCount。当_useCount减到0时则表示所有对象都回到了span,则将span释放回page cache,page cache中会对前后相邻的空闲页进行合并,得到更大的页,缓解内存碎片(外碎片)问题。

由此可见,CentralCache之所以叫做中心缓存,是因为它做到了中轴调节内存分配的工作,不同的ThreadCache没有内存找我拿,我没有找PageCache拿,ThreadCache中内存释放过多后就还一些给我,我再分配给别的ThreadCache,我分配出去的span的小对象全部都还回来了又把span还回去给PageCache合成更大的span。

6.2 CentralCache.h

//因为central cache在整个进程中只有唯一的一个,所以可以设计成单例模式
class CentralCache
{
public:
	static CentralCache* GetInstance()
	{
		return &_sInst;
	}

	//从CentralCache中对应的哈希桶中获取一个不为空的Span大块内存的对象,size代表线程申请的小对象的大小
	Span* GetOneSpan(SpanList& list, size_t size);

	//获取多个obj,batchNum代表希望拿到多少个小对象,size代表线程申请的小对象的大小
	size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);

	//size是归还的小对象的大小
	void ReleaseListToSpans(void* start, size_t size);

private:
	//跟thread cache一样规模大小的哈希桶,每个桶都是一个挂满Span对象的链表
	//每个Span对象又是被切分成小块内存的自由链表
	SpanList _spanLists[NFREELIST];

	//单例模式需要把构造函数私有化
	CentralCache()
	{}

	//单例模式需要把拷贝构造函数删除,防拷贝
	CentralCache(const CentralCache&) = delete;

	//设置成静态对象就能保证只会创建出一个对象,保证全局只有一个唯一的CentralCache对象
	static CentralCache _sInst;

};


6.2 CentralCache.cpp


//静态对象需要在类内声明,类外定义
CentralCache CentralCache::_sInst;

//从CentralCache中对应的哈希桶中获取一个不为空的Span大块内存的对象
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
	//遍历对应位置的哈希桶查找一个自由链表_freeList不为空的span
	Span* it = list.Begin();
	while (it != list.End())
	{
		if (it->_freeList != nullptr)
		{
			return it;
		}
		else
		{
			it = it->_next;
		}
	}


	//走到这里说明list遍历完了都没有找到一个有小对象的span,则需要向pagecache申请span
	//那么既然要向PageCache中获取span,那么CentralCache的桶锁就可以先解掉,为什么呢?
	//因为要去PageCache获取span对象的时候,CentralCache对应的桶可能有线程来归还span
	// 的小对象,如果没有解除桶锁的话,那么归还内存的线程就没有办法获取到桶锁,也就会被阻
	// 塞,因为我们以下的逻辑是向PageCache获取span,也就是说我们暂时不会操作CentralCache
	// 对应位置的哈希桶,所以可以先让别的线程获取到桶锁进行访问,这个过程是不会出现线程安
	//全的问题的,并且能够使不同的线程并行运行,能够提高内存池的效率

	//解掉桶锁
	list._mtx.unlock();

	//因为PageCache也是全局唯一的,并且这个NewSpan可能会是递归,所以我们不能在NewSpan内部
	//加锁,否则递归会出现死锁,所以我们在访问PageCache的时候需要先加一把大锁锁住PageCache,
	//为什么这里是加大锁锁住整个PageCache而不是在PageCache内部对应的哈希桶中加桶锁只锁住当前
	//的spanList呢?这里留个疑问,后面在PageCache的内部实现的时候再来解答
	PageCache::GetInstance()->_pageMtx.lock();
	Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
	span->_objSize = size;//记录span自由链表中每一个小内存块的大小,方便后续释放内存
	//span被分到CentralCache,说明span被使用了,置为true
	span->_isUse = true;
	PageCache::GetInstance()->_pageMtx.unlock();


	//提问:需要在这里立刻加上list的桶锁吗?
	//答案是:不用立刻加上桶锁,因为下面是对NewSpan切分的过程,这个过程中没有线程能够获取到
	//这个span,因为它还没有挂到哈希桶上,可以在切分完span之后,把它挂到对应的哈希桶的之前
	//再加上桶锁


	//需要把这个span大对象切分成一个一个的size大小的小对象挂在自由链表中
	// 
	//start是这个span对象的起始位置,可以通过span的页号换算过来
	char* start = (char*)(span->_pageId << PAGE_SHIFT);
	//bytes是这个span的大小,单位是字节,可以通过span页数换算
	size_t bytes = span->_n << PAGE_SHIFT;
	char* end = start + bytes;

	//先切分一个小对象给_freeList做头,方便尾插
	span->_freeList = start;
	void* tail = start;

	//从下一个小对象开始尾插
	start += size;


	//尾插
	while (start < end)
	{

		NextObj(tail) = start;
		tail = NextObj(tail);
		start += size;

	}

	//最后记得把tail的前4个或者8个字节的内容置空,否则里面的随机值会导致这个span对象的
	//自由链表的尾部越界访问了一些内存,最后会导致程序崩溃
	NextObj(tail) = nullptr;

	//以下是把切分好的span挂到对应的哈希桶中,所以需要重新加上桶锁,防止出现线程安全的问题
	list._mtx.lock();

	//获取到的span头插到对应位置的哈希桶
	list.PushFront(span);

	return span;
}

//获取多个obj
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
	//计算映射的哈希桶的下标
	size_t index = SizeClass::Index(size);

	//以下是获取span和从span中获取小对象,需要加上桶锁
	_spanLists[index]._mtx.lock();

	//获取一个不为空的Span,获取到的span是切分好小对象并连接到自由链表中的span
	Span* span = GetOneSpan(_spanLists[index], size);
	assert(span);
	assert(span->_freeList);

	//取span对象的自由链表的batchNum个小对象,但是不一定能取到batchNum个
	//可能不够,所以要注意end走到nullptr,这一块需要画图理解,一定画图
	start = span->_freeList;
	end = start;
	int actualNum = 1;//因为下面循环走了batchNum-1步,所以actualNum应该从1开始(方便把end->_next置空)
	int i = 0;
	while (i < batchNum - 1 && NextObj(end)!=nullptr)
	{
		end = NextObj(end);
		actualNum++;
		i++;
	}
	span->_freeList = NextObj(end);
	NextObj(end) = nullptr;//一定要注意置空

	//这个span被拿走了几个小对象,_useCount就要+=几,方便后面小对象全部释放
	//回来了的时候可以把这个span还会给PageCache
	span->_useCount += actualNum;


	_spanLists[index]._mtx.unlock();

	return actualNum;

}

void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
	//计算归还的小块内存在CentralCache中哪一个下标对应的哈希桶中
	size_t index = SizeClass::Index(size);

	//访问CentralCache中的index下标对应的哈希桶,需要加上桶锁
	_spanLists[index]._mtx.lock();

	//循环直到start链表为空,把对应的小对象都头插到对应的span中
	while (start != nullptr)
	{
		void* next = NextObj(start);

		//通过start地址的值可以转换成页号,进而通过idMapSpan找到start属于哪一个span
		Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
		//把start对应的内存块头插到对应的span的自由链表中
		NextObj(start) = span->_freeList;
		span->_freeList = start;
		span->_useCount--;

		//如果span的_useCount减到零,说明从span切分出去的所有的小对象已经全部还回来了
		//可以进一步把这个span还给PageCache,以便PageCache合并前后空闲页
		if (span->_useCount == 0)
		{
			//要把span还回去给PageCache,所以要把span从_spanLists[index]的哈希桶中删除掉
			_spanLists[index].Erase(span);
			span->_freeList = nullptr;
			span->_next = nullptr;
			span->_prev = nullptr;

			//既然要把span还给PageCache,那么就暂时不会访问_spanLists[index]哈希桶了
			//可以先把桶锁解掉,这样别的线程就能访问_spanLists[index]对应的哈希桶了
			_spanLists[index]._mtx.unlock();

			//访问PageCache需要加上大锁,防止出现线程安全的问题
			PageCache::GetInstance()->_pageMtx.lock();
			PageCache::GetInstance()->ReleaseSpanToPageCache(span);
			PageCache::GetInstance()->_pageMtx.unlock();

			//要重新上锁,因为循环不一定走完了,要继续访问CentralCache对应的桶
			//归还小对象给对应的span
			_spanLists[index]._mtx.lock();
		}

		start = next;

	}

	_spanLists[index]._mtx.unlock();

}


你可能感兴趣的:(哈希算法,算法,c语言,开发语言,c++,jvm,visual,studio)