在前文我们介绍了高并发内存池的整体框架,并且编写了thread cache部分,本文续高并发核心框架开始编写第二层框架,即centralcache。
首先是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~你懂吧?
有了以上的简单理解,我们可以尝试开始编写代码了。
对于上文明确提到的结构体---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,那么我们如何根据不同的平台定义一个变量呢?
好消息是我们可以使用条件编译,即:
#ifdef
#elif
#endif
那么ifdef我们定义什么呢?因为是根据不同的平台,所以我们要检索不同的平台有什么特别的,比如32位系统定义了_WIN32,64位系统定义了_WIN64 _WIN32,那么如果我们直接ifdef _WIN32,然后设置为某某类型。
这样肯定是不行的,因为这两个平台都设置了这个宏,所以一旦这段代码下来,那么不管是什么平台,这个变量都是我们设置的第一个。
所以,如何破局呢?实际上解决方法非常简单,我们只需要将顺序换一下即可,先判断有没有_WIN64就好了:
// 条件编译确定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管理的内存块有多少个,使用了多少个我得知道吧?所以再来个变量表示使用的数目也是正常的:
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了,对于要管理它,并且还是双向链表,我们基本上实现增删即可。
但是有一个非常重要的问题,central cache和thread cache有一个非常重要的区分点就是,central cache是会被所有线程看到的,如果是不同内存块的线程来申请内存还行,占用的桶不一样嘛。
那如果是相同内存块的线程来申请内存,这就需要锁了吧?不然安全问题就没有保障了,所以我们至少需要一个锁,那么定义在spanlist就好了,也就是说有多少个大小不同的内存块就有多少不同的锁。
那么为了方便管理,加入一个头指针:
class Spanlist
{
public:
Spanlist()
{
_head = new Span;
_head->_next = nullptr;
_head->_prev = nullptr;
}
private:
Span* _head;
public:
std::mutex _mutex;
};
而最后这个类是要使用在central cache这个类里面的,按照构造函数的调用规则,我们需要给它写上一个构造函数,然后就是非常简单的增删函数了:
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并不是某个线程独享的,它是会被所有线程看到的,所以如何保证所有线程只能看到一份变量呢?
单例模式这不就来了吗?
对于单例模式来说,有两种创建方式,一种是懒汉模式,一种是饿汉模式,我们采取简单的方式创建即可,将构造函数和拷贝私有即可。
对于它的成员变量,应该保持和thread cache一致,对于它的函数,应该有一个静态函数用来返回这个唯一实例化的对象,对于其他函数我们先简单了解,一个是获取到非空的span,一个是给到thread cache的内存块,并返回给到的内存块数目:
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文件里面定义。
CentralCache CentralCache::_sInst; // 定义是必要的
// 获取到一个非空的Span
Span* CentralCache::GetNoEmpty(Spanlist& list, size_t size)
{
return nullptr;
}
对于获取到非空的span函数涉及到了下一层,所以这里先不写。
那么对于函数FetchObj,我们需要考虑以下几个点,一个是如何给到对应的内存块,一个是加锁问题,一个给多少的问题。
对于第一个问题,给内存块的时候,我们不妨设置两个指针,一个表示内存块的开始,一个表示内存块的末尾,所以参数的start end是需要的,那么给到了之后,如何将这些内存块挂接到对应的spanlist上,只需要将end移动,然后spanlist直接接end的下一个即可,那么有些span并不是有足够的内存块,所以我们需要一个判断,并且返回对应的数目:
// 从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多个?显然不现实,所以需要控制,我们可以可以做出如下控制:
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是一个承上启下的作用,所以我们需要将下一层编写之后,才能返回来理解这里面更多的细节,本文到此结束。
感谢阅读!