C++11 memory_order_acquire 和 memory_order_release 语义?

Posted

技术标签:

【中文标题】C++11 memory_order_acquire 和 memory_order_release 语义?【英文标题】:C++11 memory_order_acquire and memory_order_release semantics? 【发布时间】:2013-04-17 07:01:32 【问题描述】:

http://en.cppreference.com/w/cpp/atomic/memory_order,以及其他 C++11 在线参考,将 memory_order_acquire 和 memory_order_release 定义为:

获取操作:当前线程中没有读取可以在此加载之前重新排序。 释放操作:当前线程中没有写入可以在此存储之后重新排序。

这似乎允许在获取操作之前执行获取后写入,这对我来说似乎很奇怪(通常的获取/释放操作语义限制了 all 的移动内存操作)。

同一在线资源 (http://en.cppreference.com/w/cpp/atomic/atomic_flag) 建议可以使用 C++ 原子和上述宽松的内存排序规则构建自旋锁互斥锁:

lock mutex: while (lock.test_and_set(std::memory_order_acquire))

unlock mutex: lock.clear(std::memory_order_release);               

有了这个锁定/解锁的定义,如果 memory_order_acquire/release 确实以这种方式定义(即不禁止重新排序后获取写入),下面的简单代码不会被破坏:

Thread1:
  (0) lock
    (1) x = 1;
    (2) if (x != 1) PANIC
  (3) unlock

Thread2:
  (4) lock
    (5) x = 0;
  (6) unlock

是否可以执行以下操作:(0) lock, (1) x = 1, (5) x = 0, (2) PANIC ?我错过了什么?

【问题讨论】:

你认为这怎么可能?您想象的事件(包括锁定和解锁)的确切顺序是什么? 我在上面的跟踪中添加了锁。我想(5)处的获取后写入可以在(4)之前执行。 release 表示“我现在完成了,这是指标”,acquire 表示“你完成了吗?看看指标” 您错过了test_and_set 是一个读取-修改-写入操作,其中有一些您没有考虑到的特殊规则。 @CarloWood "有特殊规则" 哪些规则? 【参考方案1】:

自旋锁互斥锁实现对我来说看起来不错。我认为他们对 acquirerelease 的定义完全错误。

这是我所知道的获取/发布一致性模型的最清晰解释:Gharachorloo; Lenoski; Laudon; Gibbons; Gupta; Hennessy: Memory consistency and event ordering in scalable shared-memory multiprocessors, Int'l Symp Comp Arch, ISCA(17):15-26, 1990, doi 10.1145/325096.325102。 (doi 在 ACM 付费墙后面。实际链接是指向付费墙后面的副本not。)

查看第 3.3 节中的条件 3.1 和随附的图 3:

在允许普通加载或存储访问之前 相对于任何其他处理器执行, 必须执行所有以前的获取访问,并且 在释放访问被允许执行之前 相对于任何其他处理器,所有以前的普通 必须执行加载和存储访问,并且 特殊访问[顺序]与尊重一致 彼此。

重点是:获取和释放是顺序一致的1(全局所有线程都同意获取和释放发生的顺序。)全局所有线程都同意在获取之间发生的事情并且在获取和释放之间发生了特定线程上的释放。但是允许将发布之后的正常加载和存储移动到发布之上(通过硬件或编译器),并且允许获取之前正常加载和存储在获取之后移动(通过硬件或编译器)。

(脚注 1:对于 大多数 实现来说这是正确的,但对于 ISO C++ 来说一般来说夸大了。读者线程可以不同意其他 2 个线程完成的 2 个存储的顺序。请参阅 @ 987654322@ 和this answer 了解如何为 POWER CPU 编译 C++ 的详细信息展示了在实践中与发布和获取的差异,但不是 seq_cst。但大多数 CPU 仅通过一致的缓存在内核之间获取数据,这意味着确实存在全局顺序。 )


在C++ standard(我使用了 2012 年 1 月草稿的链接)中,相关部分是 1.10(第 11 页到第 14 页)。

happens-before 的定义旨在仿照Lamport; Time, Clocks, and the Ordering of Events in a Distributed System, CACM, 21(7):558-565, Jul 1978。 C++ acquires 对应 Lamport 的 receives,C++ releases 对应 Lamport 的 sends。 Lamport 对单个线程内的事件序列进行了总排序,其中 C++ 必须允许部分排序(参见第 1.9 节,第 13-15 段,第 10 页了解 C++ 对 sequenced-before 的定义.) 不过,sequenced-before 的顺序几乎是您所期望的。语句按照它们在程序中给出的顺序排列。第 1.9 节,第 14 段:“与完整表达式相关的每个值计算和副作用都在每个值之前排序 与要评估的下一个完整表达式相关的计算和副作用。”

第 1.10 节的重点是说,无数据竞争的程序产生的值与在具有顺序一致内存的机器上运行的程序相同,并且没有编译器重新排序。如果存在数据竞争,则程序根本没有定义的语义。如果没有数据竞争,则允许编译器(或机器)对不会造成顺序一致性错觉的操作重新排序。

第 1.10 节,第 21 段(第 14 页)说:如果存在一对从不同线程对对象 X 的访问 A 和 B,则程序不是 无数据竞争,至少一个这些访问中有一个副作用,A 都不会发生在 B 之前,B 也不会发生在 A 之前。否则程序是无数据竞争的。

第 6-20 段对发生前的关系给出了非常仔细的定义。关键定义是第 12 段:

“评估A发生在评估B之前,如果:

A 在 B 之前排序,或者 线程间发生在 B 之前。”

因此,如果获取在几乎任何其他语句之前排序(在同一个线程中),那么获取必须出现在该语句之前。 (包括该语句是否执行写入。)

同样:如果几乎任何语句(在同一个线程中)发布之前排序,那么该语句必须出现在发布之前。 (包括该语句是否只进行值计算(读取)。)

允许编译器将其他计算从发布之后移动到发布之前(或从获取之前到获取之后)的原因是因为这些操作专门执行没有在关系之前发生线程间(因为它们在临界区之外)。如果他们竞相,则语义是未定义的,并且如果他们不竞相(因为它们不共享),那么您就无法准确判断它们何时发生在同步方面。

这是一个很长的说法:cppreference.com 对获取和释放的定义是完全错误的。您的示例程序没有数据竞争条件,并且不会发生 PANIC。

【讨论】:

谢谢。我熟悉这篇论文——这是我困惑的根源:获取访问/操作语义通常保证之后的加载和存储(按程序顺序)确实在获取操作之后执行。然而,cppreference 似乎允许写入以之前执行的方式出现。 我仍然不完全相信。对x 的修改不是针对原子对象,因此不是“可见的副作用”。因此,我认为由于“as-if rule”1.9/1,编译器可以重新排序或删除对 x 的操作。 (虽然在这个特定的例子中,编译器更有可能优化 x 和对 PANIC 的调用,因为不断将 x 传播到 if 测试允许完全删除 PANIC 调用,并存储在线程 2 中没有副作用。 x 的修改是可见的副作用。这只是何时副作用对其他线程可见的问题。我认为它是:发布操作将非原子副作用的可见性“广播”到其他线程。获取操作“接收”这些非原子副作用的可见性。因此,当@CedomirSegulja 的线程 1 执行 (0) 锁定时,它现在必须相信线程 2 尚未达到 (4) 或线程 2 已通过 (6)。 我会重写那个 cppreference 页面(因为我最初是根据某人的博客笔记写的)。 @Cubbi:我认为应该删除所有类型为“读取/写入线程中的读取/写入不能在原子加载/存储之前/之后重新排序”的公式。首先,我认为它们是错误的(例如,阅读器中的读取可以在原子加载之前重新排序而不会破坏 acq-rel 语义)。其次,作为一名程序员,我不关心重新排序编译器/硬件可能会做什么来提高性能,我关心的是这样一个事实,即 threadA 中的 relase 之前的语句 (sequenced-) 似乎在 (sequenced-) 的语句之前执行)在threadB完成获取之后。

以上是关于C++11 memory_order_acquire 和 memory_order_release 语义?的主要内容,如果未能解决你的问题,请参考以下文章

Ubuntu16.04编译Android5.0报了这样的错,请问怎么解决

pybind11 用于 C++14/C++17

如何为 C++0x/11 和非 C++0x/11 制作标头? [复制]

C++11 - 如何在 C++11 中制作这个 Qt 语句?

是否可以通过#define 将 g++ 设置为遵循 C++11 ISO (-std=c++11)?

C++0x 和 C++11 有啥区别?