CentralCache是高并发内存池三层结构中的第二层,承担着承上启下的关键作用。它作为所有线程共享的缓存,负责平衡各个线程的内存使用,并在ThreadCache需要时提供内存块。
1. 🏗️ CentralCache 的整体结构与作用
CentralCache 的核心作用是:
- 共享资源:所有线程的ThreadCache在需要时都会向CentralCache申请或归还内存。
- 减少锁竞争:虽然CentralCache是共享的,需要加锁,但采用了桶锁(每个哈希桶一个锁),而非全局锁,从而大幅减少锁竞争。
- 内存中转与平衡:从PageCache申请大块内存(Span),并切割成小块分配给ThreadCache;同时回收ThreadCache归还的内存,并在适当时机将空闲的Span交还PageCache进行合并,缓解内存碎片问题。
CentralCache 同样采用哈希桶结构,其映射规则与ThreadCache完全一致(如256KB以内,使用相同的对齐规则和桶数量)。但与ThreadCache每个桶挂的是自由链表不同,CentralCache的每个桶挂的是一个带头双向循环链表,链表中每个节点是一个Span
对象。
2. 📦 Span 结构设计
Span
是管理连续大块内存的基本单位,通常以页(如4KB或8KB)为单位。它在CentralCache和PageCache中至关重要。
struct Span {
PAGE_ID _pageId = 0;// 起始页号,用于标识和合并
size_t _n = 0;// 该Span管理的页数
Span* _next = nullptr;// 双向链表指针
Span* _prev = nullptr;
void* _freeList = nullptr;// 指向由该Span分割出来的小块内存自由链表
size_t _useCount = 0;// 记录已被分配出去的小块内存数量
size_t _objSize = 0;// 该Span分割出的小块内存的大小// ... 可能还有其他字段,如用于标记是否正在使用等
};
Span
的核心思想是将从PageCache拿到的大块连续内存(N页),根据其所在桶的要求,切割成一个个大小为_objSize
的小内存块。这些小块内存用自由链表连接起来,_freeList
指向链表头。_useCount
为0时,表示所有小块内存都已归还,整个Span
可以归还给PageCache。
3. 🔒 单例模式与桶锁
CentralCache在整个进程中只有一个实例,因此通常采用单例模式(饿汉模式) 实现。
class CentralCache {
public:
static CentralCache* GetInstance() {
return &_sInst;
}
// ... 其他成员函数,如FetchRangeObj, ReleaseListToSpansprivate:
SpanList _spanLists[NFREELIST];// 哈希桶数组,与ThreadCache桶数一致private:
CentralCache() {}// 构造函数私有化CentralCache(const CentralCache&) = delete;
static CentralCache _sInst;// 静态实例
};
每个SpanList
(哈希桶)都有自己的锁(std::mutex
),即桶锁。当多个ThreadCache同时访问不同的桶时不会阻塞,大大提升了并发性能。
4. ⚙️ CentralCache 核心操作
4.1 从 CentralCache 获取内存(FetchRangeObj
)
当ThreadCache的某个桶为空时,会调用FetchFromCentralCache
,进而调用CentralCache的FetchRangeObj
方法。
- 查找Span:遍历对应桶的Span链表,找到一个有空闲内存块(
_freeList
不为空)的Span。 - 批量获取:从找到的Span中,批量获取一定数量的小内存块。数量采用慢开始反馈调节算法,初始值较小,根据该ThreadCache以往的需求动态增加,避免一次分配过多用不完。
- 返回内存块:将这批内存块的首尾指针返回给ThreadCache,ThreadCache将其挂接到自己的自由链表上。
- 更新计数:增加Span的
_useCount
。 - 申请新Span:如果该桶所有Span都无空闲块,则向PageCache申请一个新的Span,进行切割并加入该桶,再从新的Span中分配。
4.2 向 CentralCache 归还内存(ReleaseListToSpans
)
当ThreadCache某个链表过长或线程销毁时,会将内存块归还给CentralCache。
- 找到对应Span:根据内存块地址和页号映射关系,确定该内存块属于哪个Span。
- 头插归还:将内存块头插回其所属Span的
_freeList
。 - 更新计数:减少Span的
_useCount
。 - Span回收:如果某个Span的
_useCount
减为0,说明所有小块内存都已归还。此时CentralCache会将该Span从对应的桶链表中摘除,并归还给PageCache。PageCache会尝试合并相邻的空闲Span,组成更大的连续内存块,解决外部碎片问题。
5. 💎 总结与下一篇预告
CentralCache 通过桶锁和Span管理,在多线程环境下高效地协调了内存的分配与回收,既减少了锁竞争,又为PageCache的内存合并提供了基础。
下一篇我们将深入最底层——PageCache(页缓存)。它将揭秘:
- 更大粒度的管理:PageCache如何以页和Span为单位管理从系统申请的大内存。
- 内存合并的奥秘:PageCache如何通过前后页合并来有效解决外部碎片问题。
- 全局唯一的单例:PageCache同样采用单例模式,是整个内存池的后备仓库。
CentralCache 的成功设计使得高并发内存池在保证多线程性能的同时,也能有效地管理内存碎片,是现代高性能C++程序中不可或缺的组件。