前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >高并发内存池 · central cache编写

高并发内存池 · central cache编写

作者头像
_lazy
发布2025-03-11 08:55:41
发布2025-03-11 08:55:41
2700
代码可运行
举报
文章被收录于专栏:Initial programmingInitial programming
运行总次数:0
代码可运行

前言:

在前文我们介绍了高并发内存池的整体框架,并且编写了thread cache部分,本文续高并发核心框架开始编写第二层框架,即centralcache。

central cache整体介绍

首先是central cache的结构,它整体的构造也是一个哈希桶映射的结构:

整体开起来几乎和thread cache没有太大的区别,要是较真说它们之间的区别,就是freelist管理的链表不一样,对于threadcache,管理的直接就是不同大小的内存块,对于central cache,管理的是span对象,那么span是什么我们暂时先不讨论。

现在关心的一个问题是:

为什么central cache和thread cache的结构那么的相似?甚至说是完全一样?

实际上,当thread cache的内存块不够的时候,就会像central cache申请内存,那么申请的时候,按照什么规则申请?是thread cache直接传一个int类型的变量,然后central cache就给它这么大的空间吗?显然不是,之所以central cache的结构和thread cache设计的这么相似,完全就是因为方便thread cache申请对象。

因为它们的结构一样,有相同的哈希映射规则,所以当thread cache的某个内存块不够了,申请的时候按照同样的方式去找,就非常非常容易的找到了。

这是它们的结构相同引发的一个讨论。

那么接下来,我们重点讨论span这个对象,相信写了这么久的代码,大家对于代码方面的命名也是非常熟悉了,所以呢,打开翻译软件看看吧~

span的意思是跨度,实际上它对应的就是地址空间中页的空间,页的内存空间跨度是非常大的,所以central cache使用这个结构体,获取到不同的页,按照不同的标准分成不同的内存块,没有了再去找span cache找。

欸,你看下一层的名字,span cache~你懂吧?

有了以上的简单理解,我们可以尝试开始编写代码了。


central cache代码编写

span编写

对于上文明确提到的结构体---span,它所需要多少成员变量?或者是什么成员函数呢?

span管理的是一连串的内存块,也就是说,我们在freelist的基础上,再给它套一层,那么管理内存块的是span,谁来管理span?

所以还需要一个单独的类来管理span,叫做spanlist好了,

那么对于span来说,我们需要管理span这个类的话不妨设置一个变量表示它是第几个span,因为一个span管理了n个页,假设一个页为8KB,而在32位系统之下,也就是2^32/8,也没有到size_t的极限,但是在64位系统下,这肯定就超出了size_t的极限了,所以我们需要将这个变量定义为unsigned long long,那么我们如何根据不同的平台定义一个变量呢?

好消息是我们可以使用条件编译,即:

代码语言:javascript
代码运行次数:0
复制
#ifdef 

#elif 

#endif

那么ifdef我们定义什么呢?因为是根据不同的平台,所以我们要检索不同的平台有什么特别的,比如32位系统定义了_WIN32,64位系统定义了_WIN64 _WIN32,那么如果我们直接ifdef _WIN32,然后设置为某某类型。

这样肯定是不行的,因为这两个平台都设置了这个宏,所以一旦这段代码下来,那么不管是什么平台,这个变量都是我们设置的第一个。

所以,如何破局呢?实际上解决方法非常简单,我们只需要将顺序换一下即可,先判断有没有_WIN64就好了:

代码语言:javascript
代码运行次数:0
复制
// 条件编译确定PAGE_ID的类型
#ifdef _WIN64
typedef unsigned long long PAGE_ID;
#elif _WIN32
typedef size_t PAGE_ID;
#endif

然后就是其他变量了, 一个span管理的页有n个,其中n是大于等于1的,所以需要控制吧?那么我们实际上分出去的是span管理的内存块,所以对于span来说,越容易管理越好,所以我们不妨来个双向链表,然后管理内存就同thread cache,使用变量_freelist管理即可,最后就是这个span管理的内存块有多少个,使用了多少个我得知道吧?所以再来个变量表示使用的数目也是正常的:

代码语言:javascript
代码运行次数:0
复制
struct Span
{
	PAGE_ID _pageid = 0; // span编号
	size_t n = 0;  // 页的数量

	Span* _next = nullptr; // 双向指针
	Span* _prev = nullptr;

	size_t _usecount = 0; // 已使用数目
	
	void* _freelist; // 管理内存块的自由链表
};

有了span之后,接下来就是谁来管理span了,那么必定是spanlist了,对于要管理它,并且还是双向链表,我们基本上实现增删即可。

spanlist编写

但是有一个非常重要的问题,central cache和thread cache有一个非常重要的区分点就是,central cache是会被所有线程看到的,如果是不同内存块的线程来申请内存还行,占用的桶不一样嘛。

那如果是相同内存块的线程来申请内存,这就需要锁了吧?不然安全问题就没有保障了,所以我们至少需要一个锁,那么定义在spanlist就好了,也就是说有多少个大小不同的内存块就有多少不同的锁。

那么为了方便管理,加入一个头指针:

代码语言:javascript
代码运行次数:0
复制
class Spanlist
{
public:
	Spanlist()
	{
		_head = new Span;
		_head->_next = nullptr;
		_head->_prev = nullptr;
	}

private:
	Span* _head;
public:
	std::mutex _mutex;
};

而最后这个类是要使用在central cache这个类里面的,按照构造函数的调用规则,我们需要给它写上一个构造函数,然后就是非常简单的增删函数了:

代码语言:javascript
代码运行次数:0
复制
	void Insert(Span* pos, Span* newSpan)
	{
		assert(pos);
		assert(newSpan);

		newSpan->_next = pos;
		newSpan->_prev = pos->_prev;
		pos->_prev->_next = newSpan;
		pos->_prev = newSpan;
	}

	void Erase(Span* pos)
	{
		assert(pos);
		assert(pos != _head); // 不能删除头指针->哨兵位
		Span* prev = pos->_prev;
		Span* next = pos->_next;
		prev->_next = next;
		next->_prev = prev;
	}

在tcmalloc的源码中,对于assert的调用还是比较频繁的,所以我们也加上。

central cache编写

上文提到的一个非常重要的观点是,central cache并不是某个线程独享的,它是会被所有线程看到的,所以如何保证所有线程只能看到一份变量呢?

单例模式这不就来了吗?

对于单例模式来说,有两种创建方式,一种是懒汉模式,一种是饿汉模式,我们采取简单的方式创建即可,将构造函数和拷贝私有即可。

对于它的成员变量,应该保持和thread cache一致,对于它的函数,应该有一个静态函数用来返回这个唯一实例化的对象,对于其他函数我们先简单了解,一个是获取到非空的span,一个是给到thread cache的内存块,并返回给到的内存块数目:

代码语言:javascript
代码运行次数:0
复制
class CentralCache
{
public:
	static CentralCache* GetInstance()
	{
		return &_sInst;
	}

	// 获取到一个非空的Span
	Span* GetNoEmpty(Spanlist& list, size_t size);
	// 从centralcache获取一定的缓存给到threadcache
	size_t FetchObj(void*& start, void*& end, size_t batchNum, size_t size);

private:
	CentralCache() {} // 私有构造函数
	CentralCache(const CentralCache&) = delete; // 删除拷贝函数
	
	static CentralCache _sInst; // 注意 这是声明

private:
	Spanlist _spanLists[NFREELIST]; // 和threadcache一样
};

单例模式这里要注意的点就是,这里因为类里面是声明,所以我们还需要在对应的cpp文件里面定义。

代码语言:javascript
代码运行次数:0
复制
CentralCache CentralCache::_sInst; // 定义是必要的

// 获取到一个非空的Span
Span* CentralCache::GetNoEmpty(Spanlist& list, size_t size)
{

	return nullptr;
}

对于获取到非空的span函数涉及到了下一层,所以这里先不写。

那么对于函数FetchObj,我们需要考虑以下几个点,一个是如何给到对应的内存块,一个是加锁问题,一个给多少的问题

对于第一个问题,给内存块的时候,我们不妨设置两个指针,一个表示内存块的开始,一个表示内存块的末尾,所以参数的start end是需要的,那么给到了之后,如何将这些内存块挂接到对应的spanlist上,只需要将end移动,然后spanlist直接接end的下一个即可,那么有些span并不是有足够的内存块,所以我们需要一个判断,并且返回对应的数目:

代码语言:javascript
代码运行次数:0
复制
// 从centralcache获取一定的缓存给到threadcache
size_t CentralCache::FetchObj(void*& start, void*& end, size_t batchNum, size_t size)
{
	size_t index = SizeClass::Index(size);// 判断索引位置
	_spanLists[index]._mutex.lock(); // 桶锁
	Span* span = GetNoEmpty(_spanLists[index], size); // 获取到一个非空的span
	assert(span);
	assert(span->_freelist);
	start = span->_freelist;
	end = start;
	size_t i = 0, actualNum = 1;
	while (i < batchNum - 1 && NextObj(end) != nullptr)
	{
		end = NextObj(end);
		i++,actualNum++;
	}
	span->_freelist = NextObj(end);
	NextObj(end) = nullptr;
	_spanLists[index]._mutex.unlock();
	return actualNum;
}

其中涉及到一个慢增长的问题,即一个固定内存块的多次请求,我应该多给多少个?

比如是一个8字节的多次请求,我一次给1000多个?那么如果是256kb的呢?还是1000多个?显然不现实,所以需要控制,我们可以可以做出如下控制:

代码语言:javascript
代码运行次数:0
复制
	static size_t NumfromCentral(size_t size)
	{
		assert(size > 0);
		size_t num = MAXBYTES / size;
		if (num < 2)
			num = 2;
		if (num > 512)
			num = 512;
		return num;
	}

同样,放在SizeClass里面。

因为central cache是一个承上启下的作用,所以我们需要将下一层编写之后,才能返回来理解这里面更多的细节,本文到此结束。

感谢阅读!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-03-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言:
  • central cache整体介绍
  • central cache代码编写
    • span编写
    • spanlist编写
    • central cache编写
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档