高并发内存池(7)- CentralCache的核心设计
高并发内存池(7)- CentralCache的核心设计
代码如下:
#pragma once#include "Common.h"// 单例模式-用饿汉模式,懒汉模式会复杂点
class CentralCache
{public://获取实例对象static CentralCache* GetInstance(){return &_sInst;}// 获取一个非空的spanSpan* GetOneSpan(SpanList& list, size_t byte_size);// 从中心缓存获取一定数量的对象给thread cachesize_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);private://和ThreadCache一样桶的数量SpanList _spanLists[NFREELIST];private://单例模式下进行构造的封装CentralCache(){}//对拷贝构造进行封装,在C++11里封装成delete函数CentralCache(const CentralCache&) = delete;//使用静态成员变量,防止冲突static CentralCache _sInst;
};
函数功能概述
这个函数从中心缓存获取一批(batchNum个)指定大小(size)的内存对象,通过start和end指针返回这批对象的起始和结束位置。
逐行解析
1. 参数说明
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
start
和end
:输出参数,返回获取的内存块链表的头和尾batchNum
:希望获取的对象数量size
:每个对象的大小
2. 找到对应的内存桶并加锁
size_t index = SizeClass::Index(size); // 根据对象大小找到对应的哈希桶索引
_spanLists[index]._mtx.lock(); // 给这个桶加锁(保证线程安全)
3. 获取一个合适的Span
Span* span = GetOneSpan(_spanLists[index], size); // 从对应桶中找一个有空闲内存的Span
assert(span); // 确保找到了Span
assert(span->_freeList); // 确保这个Span确实有空闲内存
4. 从Span的自由链表中提取对象
这是最核心的部分:
start = span->_freeList; // 记录链表的头
end = start; // 开始时头尾相同
size_t i = 0;
size_t actualNum = 1; // 已经取了1个(start)// 循环获取剩余的对象
while (i < batchNum - 1 && NextObj(end) != nullptr)
{end = NextObj(end); // end指针向后移动++i;++actualNum; // 计数增加
}
可视化过程:
Span原来的自由链表: A → B → C → D → E → nullptr第一次循环后:
start → A, end → A, actualNum=1第二次循环:
end移动到B, actualNum=2第三次循环:
end移动到C, actualNum=3如果batchNum=3,此时停止
5. 更新Span的自由链表
span->_freeList = NextObj(end); // Span的新链表从end的下一个开始
NextObj(end) = nullptr; // 截断链表,让end成为新链表的尾
span->_useCount += actualNum; // 更新Span的使用计数
继续上面的例子:
提取出的链表: start → A → B → C → nullptr (end指向C)
Span剩余链表: span->_freeList → D → E → nullptr
6. 调试代码(可移除)
// 条件断点:验证实际获取的数量是否正确
int j = 0;
void* cur = start;
while (cur)
{cur = NextObj(cur);++j;
}if (j != actualNum) // 如果计数不一致,设置断点
{int x = 0; // 这里可以下断点调试
}
7. 释放锁并返回
_spanLists[index]._mtx.unlock(); // 释放桶锁
return actualNum; // 返回实际获取的对象数量
关键理解点
1. 为什么需要batchNum?
- 批量获取减少锁竞争(一次获取多个对象,减少加锁次数)
- 提高缓存局部性(连续的内存块在一起)
2. actualNum可能小于batchNum
如果Span中的空闲对象不够batchNum个,就"有多少拿多少"。
3. 链表操作的精妙之处
start
和end
形成了一个新的链表片段- 原来的Span链表被正确截断
- 所有操作都是O(1)时间复杂度
4. 线程安全
通过桶锁(_mtx
)确保多个线程不会同时操作同一个Span链表。
简化示例
假设:
batchNum = 5
- Span当前自由链表:A → B → C → D → E → F → nullptr
执行过程:
start = A
,end = A
,actualNum = 1
- 循环4次:end移动到E,actualNum=5
- 截断链表:
- 提取出的链表:A → B → C → D → E → nullptr
- Span剩余链表:F → nullptr
- 返回5
这就是一个高效的内存分配机制,通过批量处理和精细的链表操作来优化性能。
为什么actualNum = 1这是一定的?
原因分析
1. 初始状态已经包含第一个对象
start = span->_freeList; // 已经取出了第一个对象
end = start; // 此时end也指向第一个对象
size_t actualNum = 1; // 所以计数直接从1开始
逻辑:在进入循环之前,我们已经取得了第一个对象(start
指向的对象),所以计数应该从1开始。
2. 循环条件的设置
while (i < batchNum - 1 && NextObj(end) != nullptr)
注意这里的batchNum - 1
:因为已经取得了1个对象,所以只需要再循环batchNum - 1
次就能达到目标数量。
可视化过程
假设我们要获取3个对象(batchNum = 3
),Span的自由链表为:A → B → C → D → nullptr
步骤分解:
-
初始化:
start = A // 取第一个对象 end = A // 当前链表只有A actualNum = 1 // 已经取了1个
-
第一次循环 (
i=0
, 需要再取2个):end = NextObj(end) // end从A移动到B ++i // i=1 ++actualNum // actualNum=2
现在链表:
A → B
,end
指向B -
第二次循环 (
i=1
, 需要再取1个):end = NextObj(end) // end从B移动到C ++i // i=2 ++actualNum // actualNum=3
现在链表:
A → B → C
,end
指向C -
循环结束(因为
i < batchNum-1
即2 < 2
为false) -
更新Span链表:
span->_freeList = NextObj(end) // NextObj(C) = D,所以Span剩余D→nullptr NextObj(end) = nullptr // 让C的next为nullptr,截断链表
最终得到:start → A → B → C → nullptr
,actualNum = 3
如果从0开始会怎样?
如果改成actualNum = 0
,代码会变得复杂:
start = span->_freeList;
end = start;
size_t actualNum = 0; // 从0开始// 需要额外的判断
if (start != nullptr) {actualNum = 1;// 然后循环batchNum-1次...
}
这样代码更冗长,而且需要处理start
为nullptr的特殊情况。
为什么这种设计是合理的?
- 最少有一个对象:
assert(span->_freeList)
确保了至少有一个可用对象 - 数学上的简洁性:
循环batchNum-1次
比循环batchNum次
但内部需要特殊处理更清晰 - 性能优化:减少了一次循环迭代(从1开始而不是从0开始)
特殊情况处理
代码也考虑了边界情况:
- 如果Span中对象数量不足
batchNum
,循环会在NextObj(end) == nullptr
时提前退出 - 最终返回的
actualNum
反映了实际获取的对象数量
这种设计体现了C++高性能编程中对细节的精确把控,每一个初始值的设定都有其数学和逻辑上的合理性。
这段代码实现了一个自由链表(Free List) 的管理类,这是内存池中用于管理空闲内存块的核心数据结构。我来详细解释每个部分的功能:
整体功能
FreeList
类用于管理一系列空闲的内存块,通过单链表的形式组织,支持单个和批量的内存块插入和取出操作。
核心成员变量
private:void* _freeList = nullptr; // 链表头指针,指向第一个空闲内存块size_t _maxSize = 1; // 最大可缓存的内存块数量(用于优化)size_t _size = 0; // 当前链表中空闲内存块的数量
关键方法解析
1. Push(void* obj)
- 插入单个对象
void Push(void* obj)
{assert(obj);NextObj(obj) = _freeList; // 新对象的next指向当前链表头_freeList = obj; // 链表头指向新对象++_size; // 数量增加
}
作用:将单个内存块插入到链表头部(头插法)。
可视化:
插入前: _freeList → A → B → nullptr
插入X: _freeList → X → A → B → nullptr
2. PushRange(void* start, void* end, size_t n)
- 批量插入
void PushRange(void* start, void* end, size_t n)
{NextObj(end) = _freeList; // 批量链表的尾部连接到当前链表头_freeList = start; // 链表头指向批量链表的头部_size += n; // 数量增加n个
}
作用:将一整批内存块(一个链表)插入到当前链表中。
可视化:
当前链表: _freeList → A → B → nullptr
批量插入: start → X → Y → Z → nullptr, end指向Z插入后: _freeList → X → Y → Z → A → B → nullptr
3. Pop()
- 取出单个对象
void* Pop()
{assert(_freeList);void* obj = _freeList; // 取出头节点_freeList = NextObj(obj); // 头指针指向下一个节点--_size; // 数量减少return obj; // 返回取出的对象
}
作用:从链表头部取出一个内存块(头删法)。
4. PopRange(void*& start, void*& end, size_t n)
- 批量取出
void PopRange(void*& start, void*& end, size_t n)
{assert(n <= _size);start = _freeList; // 批量链表的头就是当前链表头end = start; // 开始时头尾相同// 遍历到第n个节点for (size_t i = 0; i < n - 1; ++i){end = NextObj(end);}_freeList = NextObj(end); // 剩余链表从end的下一个开始NextObj(end) = nullptr; // 截断批量链表_size -= n; // 数量减少n个
}
作用:从链表中取出指定数量的内存块,通过start和end返回这批内存块。
可视化:
原始链表: _freeList → A → B → C → D → E → nullptr
PopRange(n=3): start → A → B → C → nullptr (end指向C)剩余链表: _freeList → D → E → nullptr
5. 辅助方法
bool Empty() { return _freeList == nullptr; } // 判断是否为空
size_t& MaxSize() { return _maxSize; } // 获取/设置最大缓存大小
size_t Size() { return _size; } // 获取当前大小
在设计中的重要作用
1. 内存池管理的核心
FreeList
是连接ThreadCache和CentralCache的关键数据结构- 用于高效管理相同大小的内存块
2. 批量操作优化
PushRange
/PopRange
减少了锁竞争和函数调用开销- 一次处理多个对象,提高缓存局部性
3. 统计信息维护
_size
用于跟踪可用内存块数量_maxSize
用于动态调整缓存策略(比如决定何时向CentralCache归还内存)
4. 线程安全考虑
- 这个类本身不是线程安全的,通常由外层的锁来保护
- 在ThreadCache中,每个线程有自己的FreeList,无需加锁
- 在CentralCache中,FreeList操作需要加桶锁
使用场景示例
// ThreadCache向CentralCache申请内存时
FreeList& list = GetFreeList(size);
if (list.Empty()) {// 从CentralCache批量获取void* start, *end;size_t actualNum = FetchFromCentralCache(start, end, batchNum, size);list.PushRange(start, end, actualNum);
}// 分配单个对象
void* obj = list.Pop();// 释放对象时
list.Push(obj);
这个FreeList
类是内存分配器高效运作的基础,通过精巧的链表操作实现了高性能的内存管理。
在C++中,.
(点运算符)和->
(箭头运算符)都用于访问对象的成员,但它们的使用场景有根本区别。这是初学者经常混淆的概念。
核心区别
运算符 | 使用场景 | 示例 |
---|---|---|
. | 用于对象实例(实际变量) | obj.member |
-> | 用于对象指针 | ptr->member |
详细解释
1. .
点运算符(用于对象实例)
当你有一个实际的对象变量(不是指针)时,使用点运算符来访问其成员。
class MyClass {
public:int value;void cout << value << endl; }
};int main() {MyClass obj; // obj是一个实际的对象obj.value = 10; // ✅ 正确:使用.访问成员变量obj.Print(); // ✅ 正确:使用.调用成员函数return 0;
}
2. ->
箭头运算符(用于对象指针)
当你有一个指向对象的指针时,使用箭头运算符来访问其成员。
class MyClass {
public:int value;void Print() { cout << value << endl; }
};int main() {MyClass* ptr = new MyClass(); // ptr是指向对象的指针ptr->value = 20; // ✅ 正确:使用->访问成员变量 ptr->Print(); // ✅ 正确:使用->调用成员函数delete ptr;return 0;
}
等价关系
箭头运算符->
实际上是解引用+点运算符的简写形式:
ptr->member 等价于 (*ptr).member
示例:
MyClass* ptr = new MyClass();// 以下三种写法完全等价:
ptr->value = 30; // 推荐:简洁明了
(*ptr).value = 30; // 正确:但较繁琐
ptr[0].value = 30; // 正确:但奇怪(指针当数组用)
常见使用场景
1. 动态分配对象
// 使用new创建对象
Student* student = new Student();
student->name = "Alice"; // 使用->
student->Study();delete student;
2. 在堆栈分配的对象
// 在栈上创建对象
Student student;
student.name = "Bob"; // 使用.
student.Study();
3. 作为函数参数
void ProcessStudent(Student* ptr) {ptr->name = "Charlie"; // 参数是指针,使用->
}void ProcessStudent(Student& ref) { ref.name = "David"; // 参数是引用,使用.(引用本质是别名)
}Student s;
ProcessStudent(&s); // 传递指针
ProcessStudent(s); // 传递引用
4. 智能指针
#include <memory>
std::unique_ptr<Student> smartPtr = std::make_unique<Student>();
smartPtr->name = "Eve"; // 智能指针也使用->
容易出错的情况
错误示例:
MyClass* ptr = new MyClass();ptr.value = 10; // ❌ 错误:ptr是指针,应该用->
ptr->value = 10; // ✅ 正确MyClass obj;
obj->value = 10; // ❌ 错误:obj是对象,应该用.
obj.value = 10; // ✅ 正确
混合使用(正确但奇怪):
MyClass* ptr = new MyClass();
(*ptr).value = 10; // ✅ 正确:但不如->简洁
记忆技巧
- 看变量声明:
- 如果变量类型有
*
(指针):用->
- 如果变量类型没有
*
(对象):用.
- 如果变量类型有
- 简单规则:
- 点用于实实在在的对象
- 箭头用于指向对象的指针
- 中文谐音:
- “点对象”(点用于对象)
- “箭头指”(箭头用于指针)
掌握这个区别对于理解C++的面向对象编程至关重要!