探秘持久内存(PMem)中无锁实现多线程安全的持久化数据结构

来源:原创
如需转载,请注明来自 MemArk 技术社区(MemArk 技术社区 - 助力先进存储架构演进)

关于作者

杨俊,博士毕业于香港科技大学计算机系,在数据库和存储系统上有超过十年的丰富研究和实践经验;现就职于第四范式任系统架构师,同时也是分级存储技术社区 MemArk (https://memark.io/) 的核心成员 。近期,由于其在持久内存编程核心社区 Persistent Memory Programming (https://pmem.io/)的卓越贡献,成为被该社区吸纳的中国第一位社区核心贡献者(reviewer)。本文将会介绍作者最近一次的代码贡献,对该社区的后续技术发展有深远影响。

一、背景

持久内存(PMem) 虽然可以保证写入PMem的数据掉电重启后不会丢失,但写入PMem的数据往往需要先写到CPU Cache里,再通过一系列CPU 指令把数据刷到PMem中。由于PMem和CPU的硬件限制,向PMem中写入大于8字节的数据并持久化无法保证写操作的原子性(即如果在持久化写入数据过程中掉电,无法保证数据完整写完),所以在PMem中保证持久化数据结构的数据一致性是非常有挑战性的。如果该持久化数据结构还需要支持正确的多线程写操作,保证数据一致性将变得更加复杂。本文主要描述此背景下的持久化数据的一致性、可见性问题。

本文适合对于持久化编程有一定基础了解的开发者阅读,本文主要包含内容:

介绍PMem无锁编程中的数据可见性、一致性问题及解决方法。
介绍本文作者最近刚合入 libpmemobj-cpp 的一个PR,专门为方便实现Single-Writer-Multiple-Reader(SWMR)多线程持久化数据结构的一种自带原子性的持久化指针(Atomic Persistent Pointer)。可在此具体参考该 PR 的讨论开发过程。

二、PMem的无锁编程

数据可见性是PMem无锁编程的非常重要的难点之一。例如某一线程使用STORE指令修改了一个内存数据,新数据可能在未持久化到PMem时(仍在CPU Cache中)就被另一线程读取。假设有如下两个线程分别使用带来原子性的atomic_write和atomic_read来写入和读取数据:

// 线程1
// pmem->a初始化为0
atomic_store(&pmem->a, 1);                      // 可见性:是, 持久化:未知
pmem_persist(&pmem->a, sizeof(pmem->a));        // 可见性:是, 持久化:是
 
// 线程2
// pmem->b初始化为0
if (atomic_load(&pmem->a) == 1) {
    pmem->b = 1;                                // 可见性:是, 持久化:未知
    pmem_persist(&pmem->b, sizeof(pmem->b));    // 可见性:是, 持久化:是
}

根据程序崩溃中止的时间点不同,我们从线程2的角度来分析a和b的值的各种可能性:

  1. 如果在线程1开始前程序中止,则:pmem->a=0,pmem->b=0
  2. 如果在两个线程都完成后程序中止,则:pmem->a=1,pmem->b=1
  3. 如果在线程1结束后程序中止,则:pmem→a=1,pmem→b=0
    探秘持久内存(PMem)中无锁实现多线程安全的持久化数据结构_第1张图片

图1:数据一致性问题

但是如图1所示,如果考虑到PMem的持久化特性带来的数据可恢复性,如果在线程1刚执行完atomic_store未执行pmem_persist时,而线程2执行完pmem_persist后系统掉电,由于pmem->a并未持久化pmem->b已持久化,将会导致pmem->a=0,pmem->b=1这种未预料到的情况发生。

导致这种数据与程序逻辑不一致的问题出现的主要原因,是线程2的操作依赖于未被持久化的数据。所以避免这种数据不一致的方法之一,就是在读取数据之后马上进行一次额外的持久化操作,比如线程2可以改成:

// 线程2
// pmem->b初始化为0
if (atomic_load(&pmem->a) == 1) {
    pmem_persist(&pmem->a, sizeof(pmem->a));    // 可见性:是, 持久化:是
    pmem->b = 1;                                // 可见性:是, 持久化:未知
    pmem_persist(&pmem->b, sizeof(pmem->b));    // 可见性:是, 持久化:是
}

这种方法可以有效避免pmem->a=0,pmem->b=1的情况出现(前提是写入线程只有一个,读者可以思考如果写入线程不只一个,此方法会有什么问题)。但是代价也不小,因为每次读取都需要进行一次额外的持久化操作。为了进一步优化性能,我们提出了一种持久化智能指针,不仅支持原子操作,且能根据实际情况,只进行必要的持久化操作。

三、支持原子操作的持久化指针

以下为支持原子操作的智能化指针的具体实现逻辑:

  1. 为了支持无锁编程,我们必须使用8字节作为持久化指针的大小,但是pmdk和libpmemobj-cpp中,公开资料中可供使用的持久化指针(persistent_ptr)是16字节的“宽指针”。在与Intel团队沟通交流后,我们了解到了一个仍处于实验测试阶段的新持久化指针:self_relative_ptr(自偏移指针)。这种持久化指针,巧妙地通过保存数据的地址与此指针对象本身在PMem中的地址之间偏移量(8字节),将持久化指针缩小为8字节,为持久化指针带来了支持原子操作的可能。
  2. 在self_relative_ptr的基础上,我们进一步发现,支持原子操作的数据地址都必须是8字节对齐的,所以其地址的低3位始终为0。于是,我们通过复用指针地址最低位,作为dirty_flag来区分读取前需要先持久化的指针。最终我们实现了一种支持原子操作的持久化指针:atomic_persistent_aware_ptr。该类最重要的两个API就是类似于atomic标准类的store/load,可以原子性的读取和写入self_relative_ptr对象,具体流程如下:

a. Store

  • 修改传入的self_relative_ptr对象所指向的地址,将最低位设为1,表示此指针未持久化
  • 将修改后self_relative_ptr中通过原子写,保存到内部的atomic对象中
    b. Load
  • 通过原子读,得到内部atomic对象中的self_relative_ptr
  • 如果该self_relative_ptr最低位为1,则进行一次持久化操作,并使用CAS(compare-and-swap,标准atomic类支持的一种原子操作,在条件成立时赋值)将内部atomic对象中的self_relative_ptr的最低位清零
  • 返回最低位清零后的self_relative_ptr
  1. 不难发现,上述操作只在Load时,self_relative_ptr最低位为1时才进行持久化操作,并在持久化后将最低位清0,有效的避免了重复进行持久化操作,且保证了数据可见时已持久化。
  2. 读者可能还注意到,我们将持久化操作与写操作(Store)进行了分离,可以说是一种对写多读少场景的优化,与此对应的,我们也实现了一个针对写少读多场景的优化。具体详情可参考源代码。

以上实现逻辑已经合入 libpmemobj-cpp,具体源代码可以参考:atomic_persistent_aware_ptr 。在此持久化指针的基础上,正确实现上述的例子将会变得非常简单,且不会出现a=0,b=1的数据不一致情况出现:

// 使用libpmemobj-cpp的transaction分配PMem对象可以保证无内存泄露
self_relative_ptr one;
try {
    pmem::obj::transaction::run(pop, [&] {
        one = nvobj::make_persistent();    //在PMem中分配一个int
        *one = 1;                               //设为1
        pmem_persist(one, sizeof(int));         //持久化
    });
} catch (...) {
    ASSERT_UNREACHABLE;
}
 
 
// 线程1
atomic_persistent_aware_ptr a;
a.store(one);                                   //直接使用atomic_persistent_aware_ptr提供的store函数写入
 
// 线程2
atomic_persistent_aware_ptr b;
if (*(a.load()) == 1) {                         //直接使用atomic_persistent_aware_ptr提供的load函数读取
    b.store(one);
}

四、阅读更多

MemArk 社区官网:https://memark.io/
分级存储技术专栏
Join Slack Workspace
扫码进入分级存储微信技术讨论群(点击打开二维码

你可能感兴趣的:(探秘持久内存(PMem)中无锁实现多线程安全的持久化数据结构)