当前位置: 首页 > news >正文

高并发内存池(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)
  • startend:输出参数,返回获取的内存块链表的头和尾
  • 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. 链表操作的精妙之处

  • startend形成了一个新的链表片段
  • 原来的Span链表被正确截断
  • 所有操作都是O(1)时间复杂度

4. 线程安全

通过桶锁(_mtx)确保多个线程不会同时操作同一个Span链表。


简化示例

假设:

  • batchNum = 5
  • Span当前自由链表:A → B → C → D → E → F → nullptr

执行过程:

  1. start = A, end = A, actualNum = 1
  2. 循环4次:end移动到E,actualNum=5
  3. 截断链表:
    • 提取出的链表:A → B → C → D → E → nullptr
    • Span剩余链表:F → nullptr
  4. 返回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

步骤分解:

  1. 初始化

    start = A    // 取第一个对象
    end = A      // 当前链表只有A
    actualNum = 1 // 已经取了1个
    
  2. 第一次循环 (i=0, 需要再取2个):

    end = NextObj(end) // end从A移动到B
    ++i              // i=1
    ++actualNum      // actualNum=2
    

    现在链表:A → Bend指向B

  3. 第二次循环 (i=1, 需要再取1个):

    end = NextObj(end) // end从B移动到C  
    ++i              // i=2
    ++actualNum      // actualNum=3
    

    现在链表:A → B → Cend指向C

  4. 循环结束(因为i < batchNum-12 < 2为false)

  5. 更新Span链表

    span->_freeList = NextObj(end) // NextObj(C) = D,所以Span剩余D→nullptr
    NextObj(end) = nullptr         // 让C的next为nullptr,截断链表
    

最终得到:start → A → B → C → nullptractualNum = 3


如果从0开始会怎样?

如果改成actualNum = 0,代码会变得复杂:

start = span->_freeList;
end = start;
size_t actualNum = 0;  // 从0开始// 需要额外的判断
if (start != nullptr) {actualNum = 1;// 然后循环batchNum-1次...
}

这样代码更冗长,而且需要处理start为nullptr的特殊情况。


为什么这种设计是合理的?

  1. 最少有一个对象assert(span->_freeList)确保了至少有一个可用对象
  2. 数学上的简洁性循环batchNum-1次循环batchNum次但内部需要特殊处理更清晰
  3. 性能优化:减少了一次循环迭代(从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;   // ✅ 正确:但不如->简洁

记忆技巧

  1. 看变量声明
    • 如果变量类型有*(指针):用->
    • 如果变量类型没有*(对象):用.
  2. 简单规则
    • 用于实实在在的对象
    • 箭头用于指向对象的指针
  3. 中文谐音
    • “点对象”(点用于对象)
    • “箭头指”(箭头用于指针)

掌握这个区别对于理解C++的面向对象编程至关重要!

http://www.dtcms.com/a/352531.html

相关文章:

  • 如何对springboot mapper 编写单元测试
  • MATLAB Figure画布中绘制表格详解
  • Cortex-M 的Thumb指令集?
  • k8s pod 启动失败 Failed to create pod sandbox
  • Il2CppInspector 工具linux编译使用
  • 算法概述篇
  • Markdown渲染引擎——js技能提升
  • MyBatis-Flex是如何避免不同数据库语法差异的?
  • 【electron】一、安装,打包配置
  • 全面赋能政务领域——移动云以云化升级推动政务办公效能跃迁
  • 【硬件-笔试面试题-61】硬件/电子工程师,笔试面试题(知识点:RC电路中的充电时间常数)
  • vue3 + jsx 中使用native ui 组件插槽
  • babel使用及其底层原理介绍
  • Java 集合笔记
  • 第二章 进程与线程
  • 简明 | Yolo-v3结构理解摘要
  • Python-机器学习概述
  • ruoyi-vue(十二)——定时任务,缓存监控,服务监控以及系统接口
  • Python 轻量级的 ORM(对象关系映射)框架 - Peewee 入门教程
  • CentOS 7 升级 OpenSSH 10.0p2 完整教程(含 Telnet 备份)
  • 性能瓶颈定位更快更准:ARMS 持续剖析能力升级解析
  • 告别繁琐运维,拥抱自动化:EKS Auto Mode 实战指南
  • C代码学习笔记(二)
  • RK3506 开发板:嵌入式技术赋能多行业转型升级
  • 大数据时代UI前端的智能化升级路径:基于用户行为数据的预测性分析
  • PMP项目管理知识点-⑨项⽬资源管理
  • 大模型应用编排工具Dify之插件探索
  • 【LeetCode - 每日1题】求对角线最长矩形的面积
  • Claude 的优势深度解析:大模型竞争格局中的隐藏护城河
  • NX773HSA19美光固态闪存D8BJND8BJQ