C++ 和持久内存技术(Persistent Memory Technologies),比如英特尔的 3D XPoint。简单来说:
什么是持久内存?
- 介于传统内存(RAM)和存储(SSD/HDD)之间的新型存储器件
- 速度远快于SSD,且断电数据依然保留(非易失性)
- 支持字节寻址,能像RAM一样直接访问数据
Intel 3D XPoint 简介
- 一种非易失性存储技术
- 比NAND闪存快1000倍
- 支持更高耐久度和更低延迟
- 可用于构建持久内存模块(比如Intel Optane DC Persistent Memory)
C++ 与持久内存的结合
- 传统C++设计假设内存是易失的,程序重启后数据会丢失
- 持久内存允许开发者将数据结构直接映射到非易失性内存区域
- 需要特别的内存模型和持久化语义,比如:
- 保证写入操作完成后数据真的写入持久层(flush、fence等指令)
- 崩溃后恢复一致的数据状态
- 目前有专门的库(如Intel PMDK,Persistent Memory Development Kit)支持C++编程
使用挑战
- 内存模型复杂,必须处理崩溃恢复和一致性
- 需要新的编程范式,如事务性持久内存(Transactional Persistent Memory)
- 标准C++还没有完全针对持久内存扩展,需要依赖第三方库
“Programming Model(编程模型)”有至少四个不同的含义:
- 硬件和软件之间的接口
硬件如何向软件暴露其功能,比如寄存器、内存结构等。 - 指令集架构(ISA)
处理器执行的机器指令集合,是硬件层面的编程模型。 - 操作系统向应用程序暴露的接口
OS提供的系统调用、API等,应用程序通过它们访问硬件资源。 - 程序员的使用体验
程序员如何感知和操作这个系统,包括编程语言、库、框架等。
这段内容是在描述**“Programming Model (meaning 3): Exposing to Applications”**,也就是操作系统如何通过不同层级向应用程序暴露持久内存(Persistent Memory)资源的方式。
大致结构是这样的:
- 最底层是硬件,比如 NVDIMM(Non-Volatile Dual In-line Memory Module,持久内存条)
- 由内核驱动(NVDIMM Driver)管理硬件设备
- 在内核空间有 持久内存感知的文件系统(Persistent Memory Aware File System)
- 这上面可能还有管理库(Management Library),帮助应用程序方便地操作持久内存
- 应用程序通过标准文件API(File API)或者更底层的直接加载/存储(Load/Store Access)
- DAX (Direct Access) 是一种允许应用程序跳过文件系统缓存,直接访问持久内存的机制,从而获得更高性能
换句话说,这描述了持久内存如何通过文件系统和内核驱动,被暴露给用户态应用程序访问的层次和路径,体现了操作系统层面的编程模型。
你说的这段内容是关于持久内存(Persistent Memory)写入路径和数据持久性保证机制,尤其是在CPU缓存和内存之间如何确保数据在断电时依然持久保存。
大致流程和关键点如下:
- MOV 指令把数据送到核心(Core)
- 数据经过CPU的多个缓存层级:L1, L2, L3缓存
- 为了保证数据持久写入内存,需要用特定指令和机制刷新缓存:
- CLWB + fence:Cache Line Write Back + 内存屏障,保证缓存行写回到内存,且写操作顺序正确
- CLFLUSH:清除缓存行,使数据写入内存
- NT stores + fence:非临时(Non-Temporal)写操作 + 屏障,直接绕过缓存写入内存
- WBINVD(仅内核可用):写回并使缓存无效,强制刷新所有缓存
- **ADR (Asynchronous DRAM Refresh)机制和WPQ (Write Pending Queue)**机制帮助在电源断电时保护内存中的数据,防止数据丢失
- 最终数据写入到DIMM(内存模块)
总结:这体现了从CPU核心发起数据写入操作,到数据穿越缓存层级,最后安全写入到持久内存的过程,以及各种保证数据不会因断电而丢失的机制。
你这段是关于持久内存(Persistent Memory)写入的原子性和持久化刷新的流程示例,以及崩溃恢复时数据一致性的问题。
大致步骤和重点:
- open(…); — 打开文件或持久内存设备
- mmap(…); — 将文件或持久内存映射到进程地址空间,得到指针
pmem
- strcpy(pmem, “Hello, World!”); — 把字符串写入映射的持久内存区域(数据写入CPU缓存,尚未真正持久化)
- pmem_persist(pmem, 14); — 显式调用持久化函数(通常会调用
clwb
+ fence 或clflush
+ fence 等指令)将缓存中的数据刷新到持久内存中,保证数据持久化 - crash — 系统或程序崩溃时,如果
pmem_persist
没有被调用,写入的数据可能仍停留在缓存中,导致断电或崩溃后数据丢失或不一致
重点: 仅写入内存地址不保证数据持久,必须调用类似pmem_persist
的函数显式刷新缓存,才能确保数据安全写入持久内存,避免崩溃后数据丢失。
如果不做正确的刷新,可能导致“部分写入”或“脏数据”,破坏数据的原子性和一致性。
持久内存写入的原子性(atomicity)问题,特别是跨越8字节(64位)存储单元时可能发生的不一致状态。
背景
- 持久内存的刷新是基于缓存行(一般是64字节)单位。
- CPU对内存的写操作通常是以8字节(64位)为原子单位,写入的数据如果跨越了两个8字节单元,写入过程就可能被中断(比如发生崩溃)。
- 因此,即使调用了
pmem_persist
来确保缓存刷写,也可能导致数据“半写”状态。
你列举的几种崩溃后可能的持久数据状态:
选项 | 描述 | 说明 |
---|---|---|
1. | \0\0\0\0\0\0\0\0\0\0... |
全是零,写入失败或没开始写 |
2. | Hello, W\0\0\0\0\0\0... |
部分写入,字符串截断 |
3. | \0\0\0\0\0\0\0\0orld!\0 |
前半部分没写,后半部分写入 |
4. | Hello, \0\0\0\0\0\0\0\0 |
部分写入,未完成 |
5. | Hello, World!\0 |
完整写入,理想情况 |
核心问题:
当写操作跨越8字节边界且发生崩溃,数据可能处于不一致的中间状态,这是持久内存开发时需要注意的。
如何保证原子性?
- 尽量避免跨越8字节边界的写操作。
- 使用事务性写入机制(比如 Intel的Persistent Memory Development Kit中的事务支持)。
- 设计日志机制(write-ahead logging)或双缓冲等方案来确保崩溃恢复后数据一致。
- 使用硬件支持的原子持久化操作(如果有)。
持久内存(Persistent Memory,PM)编程模型的分层结构和目标,特别是面向NVDIMM这类硬件的应用场景。
结构层次(从上到下):
- Application
开发者编写的应用程序,直接使用持久内存提供的能力。 - Tools
开发工具(调试器、分析器、库等),帮助开发者更高效、安全地开发。 - Standard
语言标准、持久内存规范,定义一致的接口和行为。 - Load/Store & File API
支持直接内存读写(Load/Store)或文件式访问(File API)。 - Language Runtime & Libraries
运行时环境和库,封装复杂细节,提供持久内存支持。 - PM-Aware MMU (Memory Management Unit)
支持持久内存的内存管理单元,实现地址映射与保护。 - File System & Mappings
专为持久内存设计的文件系统和内存映射,保证数据一致性和性能。 - Kernel Space
操作系统内核,提供底层管理和安全保障。
目标和结果:
- 让持久内存编程更安全、减少错误
- 让开发者使用起来更自然、符合语言习惯(idiomatic)
- 支持多种常用语言,实现统一、高效的编程体验
相关硬件:
- NVDIMM(Non-Volatile Dual Inline Memory Module):带持久性功能的内存模块,支持断电后数据保持。
**持久内存(persistent memory)和自定义分配器(pallocator)**实现的 std::vector
用法,重点在于:
关键点:
- 使用了
pvector = std::vector<p<int>, pallocator>
,也就是用带有持久内存感知的分配器pallocator
,分配持久化内存中的对象。 persistent_vector
是分配在持久内存中的vector
,数据不会随进程结束而丢失。- 你调用
push_back(42)
向向量添加元素。 - 不需要显式调用 flush(刷新缓存),这意味着分配器或底层库自动保证了数据的持久性。
- 操作是事务性的(transactional),保证一致性和原子性(即使崩溃,数据不会半更新)。
- C++ 库本身(配合持久内存支持的运行时和分配器)完成了持久化细节的管理,让程序员体验和普通
std::vector
类似。
总结:
这就是利用持久内存的现代C++抽象,让你用熟悉的容器接口操作持久化数据,而不必关心刷新缓存、写屏障等复杂细节,保证安全和高效。
你给出的代码是一个用 shared_ptr
实现的 简单线程不安全的链表队列示例,适合做持久内存(Persistent Memory)相关说明:
术语:
- Persistent Memory/Storage Class Memory/Non-Volatile Memory
快速、可按字节寻址且断电数据不会丢失的内存技术。 - Pool
进程虚拟地址空间中一块连续的持久内存区域。
代码分析:
struct entry {
std::shared_ptr<entry> next;
int value;
};
std::shared_ptr<entry> head;
std::shared_ptr<entry> tail;
void push(int value) {
auto n = std::make_shared<entry>(value, nullptr);
if (head == nullptr) {
head = tail = n;
} else {
tail->next = n;
tail = n;
}
}
int pop() {
if (head == nullptr)
throw std::runtime_error("Nothing to pop");
auto ret = head->value;
head = head->next;
if (head == nullptr)
tail = nullptr;
return ret;
}
push
添加新节点到队尾。pop
从队头移除节点并返回值。- 用
shared_ptr
管理节点生命周期,自动释放。
与持久内存的联系:
- 在持久内存上下文,这样的链表结构需要用持久内存分配器(比如
pallocator
)和持久指针(比如p_ptr
)替代shared_ptr
,保证断电后数据有效。 - 需要考虑事务性和一致性,防止崩溃时数据结构损坏。
你这段代码展示了基于**事务(transaction)**的持久内存操作,结合了持久内存库(比如 PMDK)提供的事务机制,实现原子且持久的链表入队和出队。
代码结构解析
void push(int value)
{
transaction::exec_tx(pool, [this, &value] {
auto n = make_shared<entry>(value, nullptr);
if (head == nullptr) {
head = tail = n;
} else {
tail->next = n;
tail = n;
}
});
// 事务作用域结束后,自动提交或回滚
}
int pop()
{
int ret;
transaction::exec_tx(pool, [this, &ret] {
if (head == nullptr)
throw runtime_error("Nothing to pop");
ret = head->value;
head = head->next;
if (head == nullptr)
tail = nullptr;
});
return ret;
}
transaction::exec_tx(pool, lambda)
开启一个事务,对pool
中的数据进行原子操作。- 事务内所有改动要么全成功提交,要么全部回滚,保证持久内存中数据的一致性。
push
和pop
都在事务里执行,保证链表结构不会因为意外断电而破坏。
事务特性
- Undo log:操作前记录旧状态,失败时回滚。
- ACID属性:原子性、一致性、隔离性、持久性。
- 可嵌套:支持事务内嵌套事务。
- 崩溃恢复:中断时,下一次打开pool时自动回滚或完成未完成事务。
- 锁:事务执行期间持有锁,保证数据完整性。
关联
pool
是持久内存的管理池,类似数据库中的事务日志。make_shared
可能是持久内存友好的版本,比如make_persistent
。
这段内容讲的是持久内存中**手动事务(Manual Transaction)**的用法,和自动事务(如 transaction::exec_tx
)相比,手动事务更灵活但需要显式提交。
手动事务核心示例
auto pop = pool<root>::open("/path/to/poolfile", "layout string");
{
transaction::manual(pop, persistent_mtx, persistent_shmtx);
// 在事务内做工作,修改持久内存数据
transaction::commit(); // 必须显式调用提交事务
}
// 此处锁会释放
auto aborted = transaction::get_last_tx_error(); // 检查是否有事务中断或失败
关键点总结
- 显式提交
与自动事务不同,手动事务必须调用transaction::commit()
显式提交,否则会自动回滚。 - 事务范围
事务开始于transaction::manual()
,结束于transaction::commit()
或作用域结束时自动回滚。 - 锁机制
可以传入一个或多个持久内存相关的锁,保证多线程下数据一致性。 - RAII风格
结合C++的 RAII 资源管理概念,确保事务期间资源(锁)被正确管理。 - 异常处理
默认会在异常或忘记提交时自动回滚,不会抛出异常以通知事务失败,而是通过get_last_tx_error()
获取错误信息。 - 多锁支持
允许传入任意数量的持久内存驻留锁以控制访问。
用途
手动事务适合:
- 复杂事务逻辑需要多个步骤,中间可能抛异常。
- 需要对事务提交时机有完全控制。
- 多锁协作控制更精细。
持久内存库中**自动事务(Automatic Transactions)和闭包事务(Closure Transactions)**的用法和区别。
自动事务 (Automatic Transaction) 核心示例
auto pop = pool<root>::open("/path/to/poolfile", "layout string");
try {
transaction::automatic(pop, persistent_mtx, persistent_shmtx);
// 在事务范围内进行工作,自动开始事务
// 不需要显式调用 commit()
} catch (...) {
// 处理事务失败或异常
}
auto aborted = transaction::get_last_tx_error(); // 查询事务是否失败
特点:
- 不需要显式 commit,事务会在作用域结束时自动提交(无异常)或回滚(有异常)。
- 依赖 C++17 的
std::uncaught_exceptions
来判断异常状态。 - 锁自动在事务结束时释放。
- 事务体内异常会被捕获,事务回滚,但不会直接抛出异常。
- 与手动事务功能和语义基本一致,但更简洁。
闭包事务 (Closure Transaction) 核心示例
auto pop = pool<root>::open("/path/to/poolfile", "layout string");
transaction::exec_tx(pop, [] {
// 在这里写事务体代码
}, persistent_mtx, persistent_shmtx);
auto aborted = transaction::get_last_tx_error();
特点:
- 事务体用
std::function
闭包表示,传给exec_tx
。 - 不需要显式 commit,事务由框架自动管理。
- 兼容所有支持 C++11 的编译器。
- 事务失败时抛出异常,需要用户捕获。
- 接受任意数量的锁,保证多线程安全。
- 相比自动事务更“现代”、更优雅。
- 使用闭包方式,代码更简洁,更安全。
总结对比
特性 | 手动事务 | 自动事务 | 闭包事务 |
---|---|---|---|
事务提交 | 需要显式 commit() |
自动提交或回滚 | 自动提交或回滚 |
异常处理 | 不抛异常,查询错误码 | 不抛异常,查询错误码 | 抛异常,需要捕获 |
语言标准 | C++11 或更高 | C++17 | C++11 |
锁管理 | 手动传入,多锁支持 | 手动传入,多锁支持 | 手动传入,多锁支持 |
使用便利性 | 需要管理事务生命周期 | RAII 自动管理事务生命周期 | 函数式调用,简洁明了 |
使用持久内存(如 Intel 的 PMDK)实现队列结构时,如何正确处理持久性数据的一些细节。我们重点来解释下面这两个关键词和它们的关系:
1. struct entry
中的持久化成员
最初版本:
struct entry {
shared_ptr<entry> next;
int value;
};
这个结构是可以正常用在堆上的,但不具备持久性(crash 之后数据就丢了)。如果你把它用于持久内存中,就必须考虑 如何正确保存数据(snapshot)。
2. p<T>
类型的使用:持久内存中的“快照”工具
为了让字段能在事务中持久化,就得使用 p<T>
类型来包裹它:
struct entry {
shared_ptr<entry> next;
p<int> value; // 使用 p<> 进行事务快照持久化
};
什么是 p<T>
?
这是 PMDK 提供的模板类,目的是让你能方便地对基本类型(如 int
, bool
, float
, …)实现事务内的快照+回滚机制。
它能做什么?
- 重载
operator=
:自动在赋值时记录旧值,事务失败就可以回滚。 - 重载算术/逻辑运算符:如
+
,-
,*
,==
,&&
,等价于普通类型的行为。 - 事务开始时,值被自动“快照”。
- 事务提交成功后,变更写入 NVDIMM。
- 事务失败/中断时,值被恢复。
注意点:
p<T>
适用于基本类型。- 不适合结构体或类,因为
.
运算符不能被重载 → 无法深层追踪变更。 - 如果你有复杂类型,需要自己写事务管理逻辑或用
persistent_ptr<>
来包装整块内存。
为什么 shared_ptr<entry>
不需要用 p<>
?
因为你通常会改的是 value
字段,而不是 next
指针本身。而且:
- 对象的分配/删除本身是在事务控制下完成的。
- 指针可以用
make_persistent<entry>()
和delete_persistent()
管理持久性。 - 如果你要追踪指针的值变更(很少这样做),才可能考虑
p<persistent_ptr<entry>>
。
总结
元素 | 是否需要持久化支持? | 工具 |
---|---|---|
基本类型字段(如 int) | 是 | p<T> |
指针指向的对象 | 是 | make_persistent<T>() 等 |
指针本身 | 否(通常) | 事务包裹的修改已处理 |
聚合类型(struct) | 不直接用 p<T> |
用 persistent_ptr + 自定义逻辑 |
你已经掌握了持久化队列实现中的一个关键点:部分持久化 vs. 完全持久化。我们来快速回顾并总结其中的概念和差异:
1. Partially Persistent Entry
struct entry {
shared_ptr<entry> next; // 常规指针(非持久)
p<int> value; // 持久值(snapshot 支持)
};
特点:
- 只有
value
是持久化的,通过p<int>
实现。 next
是普通的shared_ptr
,在程序崩溃后无法恢复链表结构。- 适用于只需要保证数据值持久,但不在崩溃后恢复结构的场景。
2. Fully Persistent Entry
struct entry {
persistent_ptr<entry> next; // 持久化指针
p<int> value; // 持久化数据
};
特点:
- 整个结构可持久化并能在重启后恢复。
persistent_ptr<T>
是 PMDK 提供的智能指针,它:- 在池中是一个偏移地址,启动时映射回内存。
- 在崩溃后自动恢复正确地址。
- 结构可完全恢复链表状态。
总结:p<T>
vs persistent_ptr<T>
特性 | p<T> |
persistent_ptr<T> |
---|---|---|
目的 | 快照基本类型(int/float/bool…) | 持久化对象指针 |
生命周期管理 | 不负责 | 不自动管理(需 make_persistent 等) |
持久化作用范围 | 只影响数据字段 | 用于构建和恢复对象关系(链表、树等) |
崩溃恢复后是否能恢复结构 | 否(靠 runtime rebuild) | 是(可直接 deref 重建关系) |
事务中是否自动追踪 | 是(自动快照) | 否(需手动将目标字段加到事务) |
是否支持 polymorphism | 否 | 否(不能用于虚函数或继承) |
结论
如果你要构建一个在崩溃之后依然能恢复数据结构的持久化队列,你必须使用:
struct entry {
persistent_ptr<entry> next;
p<int> value;
};
并且所有 entry
的创建和销毁都需要在事务中通过:
make_persistent<entry>()
delete_persistent<entry>()
现在看到的是将一个持久化队列的 push()
操作完整地改写为使用 PMDK 的事务机制和持久化内存分配。我们来逐步解释一下背后的关键点:
持久化队列节点结构
struct entry {
persistent_ptr<entry> next;
p<int> value;
};
意义:
persistent_ptr<entry>
:指向下一个持久节点(指针本身持久)。p<int>
:持久化的整数值,自动在事务中进行快照。
push()
方法演进回顾
初始版(非持久):
auto n = make_shared<entry>(value, nullptr); // 非持久分配
- 使用的是
shared_ptr
,数据不保存在 NVDIMM 中,程序崩溃后会丢失。
持久化版:
auto n = make_persistent<entry>(value, nullptr);
- 使用
make_persistent
分配的对象位于持久化内存中。 n
是一个persistent_ptr<entry>
,会在事务中追踪其分配。
完整 push()
方法解析:
void push(pool_base &pool, int value)
{
transaction::exec_tx(pool, [this] {
auto n = make_persistent<entry>(value, nullptr); // 分配在 PMEM 中
if (head == nullptr) {
head = tail = n; // 初始化空队列
} else {
tail->next = n; // 修改 tail
tail = n;
}
});
}
事务特性:
- 所有对 PMEM 的更改(分配节点、修改指针)都在事务中。
- 如果中途崩溃,要么完全回滚,要么全部完成。
- 事务保证原子性,避免队列处于“半插入”状态。
关键补充知识
概念 | 说明 |
---|---|
make_persistent<T>() |
在 PMEM 上分配一个 T 类型对象。必须在事务中使用。 |
persistent_ptr<T> |
智能指针,指向 PMEM 中的对象,支持偏移恢复。 |
transaction::exec_tx() |
PMDK 的事务包装器,提供强一致性。 |
pool_base &pool |
持久化内存池的句柄,事务必须绑定这个对象。 |
现在已经完成了对 持久化队列的 pop()
操作 的理解,从最初的内存结构到完整事务处理逻辑。我们来系统性地总结一下关键点,以巩固知识。
持久化 Pop 方法
int pop(pool_base &pool) {
transaction::exec_tx(pool, [this] {
if (head == nullptr)
throw runtime_error("Nothing to pop");
auto ret = head->value; // 取出值
auto tmp_entry = head->next; // 暂存下一个节点
delete_persistent<entry>(head); // 删除当前节点
head = tmp_entry; // 移动头指针
if (head == nullptr)
tail = nullptr; // 如果为空,重置 tail
return ret; // 返回值
});
}
关键点解析
项目 | 说明 |
---|---|
transaction::exec_tx() |
所有操作都在事务中,确保 crash-safe。 |
delete_persistent<entry>(head) |
在 PMEM 中销毁对象(调用析构 + 回收)。 |
make_persistent() |
在 push() 中对称使用的构造器。 |
persistent_ptr |
持久指针类型,用于定位 PMEM 中对象。 |
pool_base &pool |
表示当前持久化内存池的句柄,事务需要。 |
head == nullptr |
检查队列是否空,避免非法访问。 |
事务性分配函数总结(PMDK 专属)
函数 | 作用 |
---|---|
make_persistent<T>() |
在事务中分配并构造 PMEM 上的对象 |
delete_persistent<T>() |
在事务中析构并释放 PMEM 上的对象 |
特性 | - 事务感知(分配失败自动回滚) - 必须在 exec_tx() 或其他事务上下文中使用 |
提示
- 由于
persistent_ptr
不自动管理对象生命周期,你必须手动调用delete_persistent
。 - 每次
push()
使用make_persistent
创建新节点,因此pop()
必须负责删除节点。 - 所有成员变量和分配对象都应位于持久化内存池中(通常从
root
对象开始)。
如果你还想要: - 添加日志记录机制;
- 改造成线程安全版本(带
persistent_mutex
); - 支持队列持久化恢复测试;
持久化内存中的同步原语(如 mutex
)与标准容器(如 std::map
, std::vector
)的适配方式。以下是结构化总结,帮助你清晰掌握这部分内容。
Persistent Memory Synchronization
支持的同步类型(PMDK 提供)
类型 | 描述 |
---|---|
mutex |
类似 std::mutex ,适用于独占访问 |
shared_mutex |
类似 std::shared_mutex ,适用于读多写少 |
timed_mutex |
支持超时锁 |
condition_variable |
同步多个线程等待事件 |
接口兼容 std | 可以直接用在 std::lock_guard 等中 |
自动重新初始化 | crash 后重新挂载 pool 时自动恢复锁状态 |
可用于事务中 | 可和 transaction::exec_tx() 协同使用 |
Persistent Allocator:为容器准备
提供内容(标准接口兼容)
成员函数 | 功能描述 |
---|---|
allocate() |
分配持久化内存 |
deallocate() |
释放持久化内存 |
construct() |
调用构造函数 |
destroy() |
显式析构 |
max_size() |
支持最大分配尺寸查询 |
rebind<T>() |
为泛型容器重新绑定类型 |
使用 persistent_ptr |
所有内存引用都通过它管理(重要!) |
只能在事务内用 | 避免内存泄漏或状态不一致 |
为啥标准容器几乎直接能用?
因为:
- 有了
persistent_ptr
- 有了持久化
allocator
- 容器如
std::vector
,std::map
只需小改就可支持 PMEM!
std::map 与持久化场景
原始结构:
template <class _VoidPtr>
class __tree_node_base {
pointer __right_;
__parent_pointer __parent_;
bool __is_black_;
};
问题:
__is_black_
是关键元数据,但 不是持久化类型- 如果崩溃后恢复,会 丢失红黑树结构信息
改造方式(Injecting p<>)
改造后(PMEM 适配版):
template <class _VoidPtr>
class __tree_node_base {
typedef typename __rebind_persistency_type<pointer, bool>::type bool_type;
bool_type __is_black_; // 实际上就是 p<bool>
};
p<T>
:
- 自动重载赋值(
operator=
); - 自动在事务中 做 snapshot;
- 提供几乎所有算术和逻辑操作;
- 用于基本类型:
bool
,int
,float
等。
总结一句话:
通过
persistent_ptr
和p<T>
的结合,再配合持久化 allocator,就可以把std
容器改造为 crash-consistent 持久化容器。
如果你希望了解:
- 如何自己封装一个
std::map
持久化版本 - 如何同步多个持久化队列
- 或者如何用
condition_variable
实现持久化生产者-消费者