六、高并发内存池–Central Cache
6.1 Central Cache的工作原理
central cache也是一个哈希桶结构,他的哈希桶的映射关系跟thread cache是一样的。不同的是他的每个哈希桶位置挂是SpanList链表结构,不过每个映射桶下面的span中的大内存块被按映射关系切成了一个个小内存块对象挂在span的自由链表中。如果在thread cache中申请不到内存就会到central cache的同一个位置申请,thread cache和central cache的哈希桶的映射关系是完全一致的。
中心缓存central cache是如何工作的?
申请内存:
-
当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中申请内存对象的概率不会很高,因此桶锁的竞争也不会特别激烈,所以使用桶锁能尽可能提高效率。 -
central cache映射的spanlist中所有span的都没有内存以后,则需要向page cache申请一个新的span对象,拿到span以后将span管理的内存按大小切好用自由链表链接到一起。然后从span中取对象给thread cache。
-
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();
}