危险指针的内存排序

Posted

技术标签:

【中文标题】危险指针的内存排序【英文标题】:Memory ordering for hazard-pointers 【发布时间】:2020-06-07 03:55:23 【问题描述】:

以下代码是在显着简化 hazard-pointer 算法(在this 论文中介绍)之后可以得到的。由于大量的简化,它不能用来代替算法(回答这个问题不需要知道任何关于算法的知识)。但是,我相信它仍然完美地代表了原始算法中的内存排序挑战。

所以问题是最好的内存排序是什么,这样如果ptr->a = 1; 被执行,结果就不会是未定义的(order1 ... order5 的值)?

struct T  int a = 0; ;
static_assert(std::is_trivially_destructible_v<T>);
std::atomic<T*> anew T();
std::atomic<T*> hnullptr;

// Thread 1
auto ptr = a.load(order1);
h.store(ptr,order2);
if(ptr == nullptr || ptr != a.load(order3))
  return;
ptr->a = 1;

// Thread 2
auto ptr = a.exchange(nullptr,order4);
if(ptr != h.load(order5))
  delete ptr;

我们知道要执行 ptr-&gt;a=1;a.exchange 必须在第二个 a.load 之后发生(即使是宽松的内存排序也能保证这一点)。但是,问题是如何确保h.load 会看到h.store 的效果。即使我们只在各处使用顺序内存排序,我也无法弄清楚为什么代码可以工作。

【问题讨论】:

【参考方案1】:

为简单起见,这些论文通常假设一个顺序一致的内存模型 - 您引用的论文也是如此。您的示例被高度简化,但它仍然包含危险指针算法的要点。您必须确保线程 2“看到”线程 1 存储的危险指针(即线程 1 已获得安全引用),或者线程 1 看到 a 的更新值。

在我的论点中,我将使用以下符号 - a -sb-&gt; b 表示“a 在 b 之前排序” - a -sco-&gt; b 表示“在所有顺序一致操作的单个总顺序 S 中 a 先于 b” - a -rf-&gt; b 表示“b读取a写入的值”(reads-from)

让我们假设所有原子操作都是顺序一致的。这将产生以下情况:

线程 1:a.load() -sb-&gt; h.store() -sb-&gt; a.load() -sb-&gt; ptr-&gt;a=1 线程 2:a.exchange() -sb-&gt; h.load() -&gt; delete ptr

由于顺序一致的操作是完全有序的,我们必须考虑两种情况:

h.store() -sco-&gt; h.load() 这意味着h.store() -rf-&gt; h.load(),即线程 2 保证“看到”写入线程 1 的危险指针,因此它不会删除 ptr(因此线程 1 可以安全地更新ptr-&gt;a)。

李>

h.load() -sco-&gt; h.store() 因为我们还有a.exchange() -sb-&gt; h.load()(线程2)和h.store() -sb-&gt; a.load()(线程1),这意味着a.exchange() -sco-&gt; a.load()a.exchange() -rf-&gt; a.load(),即线程1保证“看到”a的更新值(因此不会尝试更新ptr-&gt;a)。

因此,如果所有操作都是顺序一致的,那么算法就会按预期工作。但是,如果我们不能(或不想)假设所有操作都是顺序一致的怎么办?可以放宽一些操作吗?问题是我们必须确保两个不同线程中两个不同变量(ah)之间的可见性,这需要获取/释放可以提供的更强保证。但是,如果您引入顺序一致的栅栏,则可以放宽操作:

// Thread 1
auto ptr = a.load(std::memory_order_acquire);
h.store(ptr, std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_seq_cst);
if(ptr == nullptr || ptr != a.load(std::memory_order_relaxed))
  return;
ptr->a = 1;

// Thread 2
auto ptr = a.exchange(nullptr, std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_seq_cst);
if(ptr != h.load(std::memory_order_relaxed))
  delete ptr;

所以我们有以下情况:

线程 1:a.load() -sb-&gt; h.store() -sb-&gt; fence() -sb-&gt; a.load() -sb-&gt; ptr-&gt;a=1 线程 2:a.exchange() -sb-&gt; fence() -sb-&gt; h.load() -&gt; delete ptr

标准规定:

对于原子对象M上的原子操作AB,其中A修改M B 取它的值,如果有 memory_order_seq_cst 栅栏 XY 使得 A 是排在X之前,Y排在B之前,X排在Y之前S,然后 B 观察 A 的效果或 M 在其修改顺序中的后续修改。

栅栏也是单个总订单的一部分S,所以我们再次需要考虑两种情况:

Thread1 fence -sco-&gt; Thread 2 fence 由于h.store() -sb-&gt; fence()(线程1)和fence() -sb-&gt; h.load()(线程2)保证线程2“看到”线程1写入的危险指针。 Thread 2 fence -sco-&gt; Thread 1 fence 由于a.exchange() -sb-&gt; fence()(线程2)和fence() -sb-&gt; a.load()(线程1)保证线程1“看到”a的更新值。

后来的版本正是我在xenium 库中实现危险指针的方式。

【讨论】:

以上是关于危险指针的内存排序的主要内容,如果未能解决你的问题,请参考以下文章

C 语言二级指针作为输入 ( 自定义二级指针内存 | 二级指针 排序 | 通过 交换指针方式 进行排序 )

C 语言二级指针作为输入 ( 自定义二级指针内存 | 二级指针排序 | 抽象业务逻辑函数 )

C 语言二级指针内存模型 ( 指针数组 | 二维数组 | 自定义二级指针 | 将 一二 模型数据拷贝到 三 模型中 并 排序 )

C 语言二级指针作为输入 ( 二维数组 | 二维数组遍历 | 二维数组排序 )

带危险指针的无锁内存回收

排序一次查找两元素