分享干货,主打一个真实
各位老师好,我是小义。
本文主要描述 DeepSeek-ai/3FS对象池(ObjectPool)双层缓存架构的实现
图1-了解需求
阅读本文我将获得以下收益,希望对你也有帮助:
✅ 利用C++标准库提供的vector、unique_ptr等工具,可以快速开发一个高效优雅的组件,一行代码搞定最难部分,足够简单。
✅ 利用C++提供的thread_local标记,能否模拟CPU三级缓存设计(L1、L2单核独有,L3多核共享)?
能实现:
static T l3
,不需要锁。thread_local tls l2(l3)
,同样一行代码搞定,不需要锁。 ✅ 单元测试的重要性不容忽视。
让我们开始吧。
风和日丽的中午,老王来到小义办公桌前说:
"最近上线的Ceph集群SWAP使用率偏高,导致读写业务延迟增加。原来128G内存(见下图)已经增加到256G作为临时解决方案。根据公司会议安排,请你预研一下内存池分配方案。"
free -h
total used free shared buff/cache available
Mem: 128G 115G 3.2G 512M 9.8G 12G
Swap: 16G 8.0G 8.0G
小义迅速记录了要点。
图1-了解需求
如何实现这个需求?根据已有资料,小义整理了几种思路:
方向 | 预计耗时 | |
---|---|---|
思路1 | 学习gcc-mirror/gcc | 1年,结果未知 |
思路2 | 学习google/tcmalloc | 半年,结果未知 |
思路3 | 学习STL源码剖析 | 3个月,结果未知 |
思路4 | 学习经典数据库内存设计 | 1个月,结果未知 |
老王看到后直接否决:"这太复杂了,投入这么多精力但结果未知,我怎么评估?回去继续调查,我不会批准的。越简单越好!"
小义开始自我鼓励:
划重点:在阅读代码之前,应该问自己三个问题
在高并发系统中,采用线程局部缓存的对象池相比直接使用全局池通常能提供:
分级存储
类图:
+--------------------------------------+
| ObjectPool<T> |
+--------------------------------------+
| 线程本地存储 (TLS) | 全局存储 |
| +----------------------+ +----------+ |
| | first_batch | | global_ | |
| | (主缓存队列) | | (批次队列)| |
| +----------------------+ +----------+ |
| | second_batch | | mutex_ | |
| | (溢出队列) | | | |
| +----------------------+ +----------+ |
+--------------------------------------+
↑ ↑
| |
+---------------+ +------------------+
| Ptr (智能指针) | | Storage (存储块) |
| with Deleter | +------------------+
+---------------+
可视化:
进程内存空间
├── 共享区域
│ └── static ObjectPool instance (单个实例,共享)
│ ├── mutex_
│ └── global_
│
├── 线程1私有区域
│ └── thread_local TLS tls (线程1专属)
│ ├── first_ (线程1专属批次)
│ └── second_ (线程1专属批次)
│
├── 线程2私有区域
│ └── thread_local TLS tls (线程2专属)
│ ├── first_ (线程2专属批次)
│ └── second_ (线程2专属批次)
└── ...
原因在于:
只有在两种情况下才需要同步:
申请读取操作
文本绘制流程图:
┌─────────────────┐
│ 调用 ObjectPool::get() │
└──────────┬──────┘
↓
┌─────────────────┐
│ 获取线程本地存储 TLS │
└──────────┬──────┘
↓
┌─────────────────┐
│检查 second_ 批次 │
└──────────┬──────┘
↓
┌───────────────────────┐
│ second_ 有对象? │
└───────────┬───────────┘
Yes │ No
┌─────┘ └────┐
↓ ↓
┌──────────────┐ ┌──────────────┐
│从second_取对象│ │检查first_批次│
└───────┬──────┘ └───────┬──────┘
│ │
↓ ↓
┌───────────────┐ ┌───────────────────┐
│ 返回构造的对象 │ │ first_ 有对象? │
└───────────────┘ └────────┬──────────┘
Yes │ No
┌─────┘ └────┐
↓ ↓
┌──────────────┐ ┌───────────────┐
│从first_取对象│ │尝试从全局获取批次│
└───────┬──────┘ └───────┬───────┘
│ │
↓ ↓
┌───────────────┐ ┌───────────────┐
│ 返回构造的对象 │ │ 获取成功? │
└───────────────┘ └───────┬───────┘
Yes │ No
┌────┘ └────┐
↓ ↓
┌────────────────┐ ┌────────────────┐
│从批次获取并返回│ │创建kThreadLocalMaxNum个对象│
└────────────────┘ └─────────┬──────┘
↓
┌────────────────┐
│从批次获取并返回│
└────────────────┘
普通操作
文本绘制流程图:
┌─────────────────┐
│ 对象销毁触发 │
│ Deleter::operator() │
└──────────┬──────┘
↓
┌─────────────────┐
│ 调用对象析构 │
└──────────┬──────┘
↓
┌─────────────────┐
│ 获取线程本地存储 TLS │
└──────────┬──────┘
↓
┌─────────────────┐
│ first_ 满了? │
└───────────┬─────┘
No │ Yes
┌─────┘ └────┐
↓ ↓
┌──────────────┐ ┌──────────────┐
│放入first_批次│ │放入second_批次│
└──────────────┘ └───────┬──────┘
↓
┌───────────────────┐
│second_ 达到阈值? │
└────────┬──────────┘
No │ Yes
┌─────┘ └────┐
↓ ↓
│ ┌──────────────────┐
│ │整批移入全局缓存 │
│ └─────────┬────────┘
│ ↓
│ ┌──────────────────┐
│ │清空并预留second_ │
│ └──────────────────┘
↓
┌───────────────┐
│ 结束 │
└───────────────┘
加锁全局操作:
全局操作
using Ptr = std::unique_ptr<T, Deleter>; // 唯一指针
static Ptr get() {
// 无需加锁
return Ptr(new (tls().get().release()) T);
}
// 伪代码表示
void* memory = get();
ptr = operator new(memory) T
std::unique_ptr(ptr)
static auto &tls() {
static ObjectPool instance; // 单例模式,C++11中推荐的线程安全的单例实现方式
// 疑问1:线程安全吗?多个线程同时调用tls()在第一次初始化时?不需要if判断吗?
// 安全的。C++11中静态局部变量初始化是线程安全的
// C++11静态局部变量的线程安全性依赖于编译器生成的隐式同步代码,
// 通过守护变量和原子操作实现高效的线程安全初始化
// 这是单例对象,整个程序中只有一个实例
thread_local TLS tls{instance};
// 疑问2:
// 在C语言中,定义三类线程,不同线程使用不同互斥锁、不同条件变量、不同线程局部变量,
// 这是最简单、最不容易出错的方式,但都是写死的,扩展性不高。
// TLS tls真的为不同线程创建不同副本吗?
// C++11 thread_local修饰TLS变量
// 保证了parent是共享的,而其他成员在不同线程中是私有的
return tls;
}
class TLS {
public:
TLS(ObjectPool &parent): parent_(parent) {}
private:
ObjectPool &parent_; // 指针类型,占用空间大小8字节
Batch first_; // 向量,线程独有
Batch second_; // 向量,线程独有
};
划重点:
static ObjectPool instance
:不要为初始化编写自己的双重检查锁定thread_local TLS
:不同线程使用不同资源线程A调用 get():
线程B同时调用 get():
疑问:
// 线程A
Ptr get() {
// 1. 获取线程A的TLS
TLS& threadLocalStorage = tls(); // 返回线程A专属的TLS
// 2. 从线程A的本地缓存获取对象
std::unique_ptr<Storage> storage = threadLocalStorage.get();
// 内部流程:
// - 先检查second_批次(线程A专属)
// - 再检查first_批次(线程A专属)
// - 如缓存为空,才从全局instance获取新批次
// 3. 构造并返回对象
return Ptr(new (storage.release()) T(...));
}
// 线程B
Ptr get() {
// 1. 获取线程B的TLS
TLS& threadLocalStorage = tls(); // 返回线程B专属的TLS
// 2. 从线程B的本地缓存获取对象
std::unique_ptr<Storage> storage = threadLocalStorage.get();
// 内部流程:
// - 先检查线程B的second_批次
// - 再检查线程B的first_批次
// - 如线程B的缓存为空,才访问全局instance
// 3. 构造并返回对象
return Ptr(new (storage.release()) T(...));
}
get操作的具体实现:
// 从线程本地缓存或全局缓存获取对象
std::unique_ptr<Storage> get() {
// 从second批次中弹出
if (!second_.empty()) {
auto item = std::move(second_.back());
// 疑问:为什么要std::move?因为vector存放的是unique_ptr
second_.pop_back();
return item; // 类似CPU的L1缓存
} // using Batch = std::vector<std::unique_ptr<Storage>>;
// 当本地和全局缓存都为空时分配一个批次
// L3需要加锁
if (first_.empty() && !parent_.getBatch(first_)) {
// 处理空缓存情况
}
// 从first批次中弹出
// 类似CPU的L2缓存
auto item = std::move(first_.back());
first_.pop_back();
return item;
}
// 从全局获取
bool getBatch(Batch &batch) {
auto lock = std::unique_lock(mutex_);
// 从全局缓存中弹出一个批次
batch = std::move(global_.back());
global_.pop_back();
return true;
}
创建 销毁
│ │
▼ ▼
┌────────┐ ┌─────────┐ ┌────────┐ ┌────────┐ ┌─────────┐
│ get() │───►│ 使用对象 │───►│ ~Ptr() │───►│Deleter │───►│ put() │
└────────┘ └─────────┘ └────────┘ └────────┘ └─────────┘
│ │
▼ ▼
┌────────┐ ┌─────────┐
│tls.get()│ │tls.put()│
└────────┘ └─────────┘
当智能指针释放时调用:
struct Deleter {
constexpr Deleter() = default;
void operator()(T *item) {
item->~T(); // 调用析构函数
// 将内存重新解释为Storage并归还池中
tls().put(std::unique_ptr<Storage>(reinterpret_cast<Storage *>(item)));
}
};
// 疑问1:为什么要用reinterpret_cast?
// 答:using Storage = std::aligned_storage_t<sizeof(T), alignof(T)> 解决了对齐问题
// 疑问2:item->~T() 可以单独抽出来,不执行free会内存泄漏吗?
// 答:不会,因为内存被重用而不是释放
划重点:
单元测试确保了代码的正确性和稳定性。
TEST(TestObjectPool, AllocateAndRelease) {
constexpr auto N = 1000000;
static size_t constructTimes = 0;
static size_t destructTimes = 0;
struct A {
A() { ++constructTimes; }
//call?
~A() { ++destructTimes; }
//call?
};
//百万级别的分配/释放操作验证了系统在高压下的稳定性
for (auto i = 0; i < N; ++i) {
ObjectPool<A>::get();
//get 调用构造函数,返回一个智能指针,
//智能指针在析构时会调用析构函数
//单线程环境 thread_local TLS tls{instance}
//tls 在每个线程中是唯一的,因此可以保证在单线程环境下的线程安全
}
ASSERT_EQ(constructTimes, N);
ASSERT_EQ(destructTimes, N);
//如果存在内存泄漏,析构次数将小于构造次数
}
为什么要自定义:std::unique_ptr默认的销毁策略是std::default_delete
// https://en.cppreference.com/w/cpp/memory/unique_ptr
// https://en.cppreference.com/w/cpp/memory/default_delete
template<
class T,
class Deleter = std::default_delete<T> // 默认参数
> class unique_ptr;
默认的delete操作:调用析构函数并释放内存
// https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/unique_ptr.h
/**
* default_delete的主模板,
* 被unique_ptr用于单个对象
*
* @headerfile memory
* @since C++11
*/
template<typename _Tp>
struct default_delete {
void operator()(_Tp* __ptr) const
{
delete __ptr;
// 为避免资源泄漏,
// 每次动态分配必须有相应的释放操作
}
};
划重点:delete操作执行了什么?
string *ps = new string("Memory Management");
// 等价于:
void *memory = operator new(sizeof(string));
string::string("Memory Management"); // 调用构造函数
delete ps;
// 等价于:
ps->~string(); // 调用对象的析构函数
operator delete(ps); // 释放内存
仅释放相关句柄占用的空间是不够的,还需要执行close操作
template<typename Ty>
class Deleter {
public:
void operator()(Ty *ptr) const {
cout << "Call a custom method !!!!! " << endl;
fclose(ptr); // 关闭文件
}
};
// 如何使用unique_ptr的自定义销毁器成员?
int main() {
// lambda也可以作为销毁器
std::unique_ptr<FILE, Deleter<FILE>> ptr(fopen("data.txt", "w"));
return 0;
}
class FreeMemory {
public:
void operator()(int* p) {
std::cout << "delete memory" << std::endl;
delete p;
}
};
std::unique_ptr<int, FreeMemory> up1(new int(0));
最佳实践:
struct Deleter {
void operator()(T *item) {
item->~T(); // 只执行析构函数,不释放内存
tls().put(std::unique_ptr<Storage>(reinterpret_cast<Storage *>(item)));
}
};
ObjectPool设计做到了足够简单且高效,通过巧妙利用C++11特性实现了无锁化的高性能对象池。
--------------------END--------------------------
刚刚好,是最难得的美好
我就在这里,我刚刚好。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有