c++ 在单个线程中,放松的原子内存访问的顺序保证是什么?

k4emjkb1  于 2023-05-24  发布在  其他
关注(0)|答案(1)|浏览(135)

我一直在探索C的世界很长一段时间,我对以下问题感兴趣。我感兴趣的是正式的答案(与链接到C标准确认您的答案)。我希望你会对这个不简单的问题感兴趣:)
所以A是一个全局变量。其他线程只读取它。

void foo() {
  // here A != 42
  A.store(42, std::memory_order::relaxed);
  auto a = A.load(std::memory_order::relaxed);
  if (a != 42) assert("What!?")
}

一些线程调用foo()。什么语言规则保证A.store(42, ...)在调用foo '的线程中发生在A.load(...)之前(更准确地说是sequenced before)? 现在让我们添加某些B到问题条件。它也是一个全局std::atomic变量。 现在foo()`函数看起来像这样:

void foo() {
  // here A != 42
  A.store(42, std::memory_order::relaxed);
  B.store(0, std::memory_order::seq_cst);
  auto a = A.load(std::memory_order::relaxed);
  if (a != 42) assert("What!?")
}

在x64上,我可以确定这样的代码等价于(除了我们不修改B:D):

void foo() {
  // here A != 42
  A.store(42, std::memory_order::relaxed);
  std::atomic_thread_fence(std::memory_order::seq_cst);
  auto a = A.load(std::memory_order::relaxed);
  if (a != 42) assert("What!?")
}

但是从 C++ 标准的Angular 来看,是否有这样的保证呢?

ztigrdn8

ztigrdn81#

什么语言规则保证A.store(42, ...)在调用foo '的线程中发生在A.load(...)之前(更准确地说,顺序在A.load(...)`之前)?
这一个实际上 * 是 * 简单:)参见[介绍.执行] p9:“与完整表达式关联的每个值计算和副作用都在与要计算的下一个完整表达式关联的每个值计算和副作用之前排序。”

  • A.store(42, std::memory_order::relaxed)是[intro.execution] p5.6下的全表达式(“不是另一个表达式的子表达式并且不是全表达式的一部分的表达式。”)。
  • auto a = A.load(std::memory_order::relaxed);是一个init-declarator,因此在p5.4下是一个全表达式。

Sequenced-before是程序顺序,简单明了。这不是记忆排序的微妙部分。你的A.store绝对发生在你的A.load之前,你的Assert永远不会失败。
C++内存模型不会改变单线程程序的语义--它们仍然是“自然”语义--它也不会改变单线程内访问的语义。否则几乎不可能编程。
换个Angular 来看:如果你写了

int b;
b = 42;
int a = b;
assert(a == 42);

你就不会问这个问题了对吧原子变量的语义比非原子变量的语义更强,即使是在宽松的顺序下。所以任何工作(即是定义良好的),如果将非原子变量升级为原子变量,则无论您使用什么memory_order,都将仍然有效。
有时会产生困惑的地方是在意识到之前发生的事情的真正含义时。有些人,当他们意识到这一点时,认为它使内存排序变得微不足道,因为他们认为“X发生在Y之前”告诉你“X总是在Y之前被观察到”。不是这个意思它告诉你的只是“Y将观察X”。
至于你的第二个问题,不,一般来说,不相关的seq_cst访问并不意味着seq_cst围栏。
即使是在x86上,也是如此。在x86上,StoreStore、LoadLoad和LoadStore重新排序已经是不可能的,所以seq_cst只需要防止StoreLoad重新排序。这可以简单地通过确保在每个seq_cst存储和每个seq_cst加载之间,至少有一个屏障指令(例如,mfence,尽管在x86上,一个不相关的锁定RMW指令也充当了屏障)。编译器可以通过两种方式完成此操作:

  1. seq_cst负载只发出普通负载; X1 M14 N1 X存储发出普通存储,其后跟随屏障。
  2. seq_cst负载发出屏障,随后是普通负载; seq_cst存储器仅发出普通存储器。
    因此,如果编译器遵循策略#2,那么存储到B将只是一个普通的存储指令,看不到任何障碍。它具有通常的发布语义,但仍然可以使用后续指令重新排序,例如您的A.load()
    现在,在你的程序中,这并没有太大的意义。就这点而言,围栏也不会。在对 same 变量的访问之间放置栅栏或其他屏障并不能真正实现任何东西。如果A是程序中线程之间共享的唯一变量,那么添加或删除栅栏(或B.store())不会以任何方式改变程序的可能行为。栅栏仅在您将它们放置在对 * 不同 * 变量的访问之间时才有用。如果你的程序中还有其他的访问没有显示出来,那么在那一行上设置一个栅栏可能会有所不同,但是我们必须看到程序的其余部分才能说得更多。
    例如,假设您有
A.store(42, std::memory_order_relaxed);
B.store(0, std::memory_order_seq_cst);
C.store(17, std::memory_order_relaxed);

由于seq_cst意味着release,因此A和B存储不能彼此重新排序。然而,C可以在B之前重新排序,然后在A之前重新排序。所以另一个执行c = C.load(acquire); a = A.load(acquire);的线程可以得到c == 17 && a == 0。这在x86上甚至是可能的,因为C++内存模型允许这种行为,编译器被允许进行重新排序并发出mov [c], 17 ; mov [a], 42; mov [b], 0。但是如果用releaseseq_cst围栏代替B.store(),那么c == 17 && a == 0在任何兼容的实现上都是不可能的。

相关问题