C++原子性:函数调用是否起到内存屏障的作用?

7

我正在阅读这篇文章《编译时的内存排序》,其中提到:

事实上,大多数函数调用都充当编译器屏障,无论它们是否包含自己的编译器屏障。这不包括内联函数、带有 pure 属性声明的函数以及使用链接时代码生成的情况。除了这些情况外,对外部函数的调用甚至比编译器屏障更强,因为编译器不知道函数的副作用是什么。

这是一个真实的陈述吗?看看这个示例 -

std::atomic_bool flag = false;
int value = 0;

void th1 () { // running in thread 1
  value = 1;
  // use atomic & release to prevent above sentence being reordered below
  flag.store(true, std::memory_order_release);
}

void th2 () { // running in thread 2
  // use atomic & acquire to prevent asset(..) being reordered above
  while (!flag.load(std::memory_order_acquire)) {}
  assert (value == 1);    // should never fail!
}

然后我们可以删除原子性,但用函数调用来替代 -
bool flag = false;
int value = 0;

void writeflag () {
  flag = true;
}
void readflag () {
  while (!flag) {}
}
void th1 () {
  value = 1;
  writeflag(); // would function call prevent reordering?
}
void th2 () {
  readflag();  // would function call prevent reordering?
  assert (value == 1);    // would this fail???
}

有什么想法吗?

1
没有这样的事情是真的;你提出的代码存在竞争条件。 - Kerrek SB
2
不行,不行,不行!你不能同时从2个或更多线程中读写flag。默认情况下,对bool的操作不保证是原子性的。你需要将该标志设置为原子性。 - Arunmu
4个回答

4

编译器障碍不同于内存障碍。编译器障碍防止编译器越过障碍移动代码。内存障碍(宽泛来说)防止硬件在障碍处移动读取和写入操作。对于原子操作,您需要同时使用两者,并且还需要确保在读取或写入时值不会被分离。


在上述情况下,无论读/写操作是否在单独的函数调用中,原子和内存顺序选项始终是必要的吗?该文章有点误导。 - Jeffery
std::atomic_thread_fence(std::memory_order_seq_cst) 或者 x86 _mm_mfence() 这样的内存屏障即使是内嵌函数,也包含编译器屏障,否则它就不太可用。 - Peter Cordes
使用std::memory_order_relaxed的原子操作不需要任何屏障,只需保证不撕裂/仅访问一次的原子性保证。 - Peter Cordes

2

严格来说,不是的,因为链接时代码生成是一种有效的实现选择,不必是可选的。

还有第二个问题,那就是逃逸分析。声明是这样的:"编译器不知道函数的副作用是什么。"但如果没有指向我的本地变量的指针从我的函数中逃逸出去,那么编译器确信其他函数不会改变它们。


不仅如此:一个计算的结果被丢弃并被简单地删除。(即使在Ada中。即使它在Ada中会引起异常。)因此,您无法可靠地在基准测试中计时明显无用的计算。有些计算不仅可以在“编译器屏障”周围重新排序,而且还可能被删除。这使得整个问题是否可以“跳过”屏障变得非常愚蠢,因为它们可以“跳入”黑洞。 - curiousguy
在一些编译器(例如GCC)中,编译器屏障(如asm("":::"memory"))不会影响未转义的局部变量。恰恰是因为没有任何东西能够区分它们。如果需要对它们进行屏障处理,请将它们作为 "m"(var) 输入添加到 asm 语句中以将其强制存储到内存中。 - Peter Cordes

1
你混淆了用于线程间内存可见性的内存屏障和编译器屏障,编译器屏障不是线程设备,只是一个防止编译器重新排序副作用的设备(或技巧)。
你需要为你的线程示例使用内存屏障。
你可以使用编译器屏障来确保在本地CPU上执行内存副作用的顺序(用于其他目的,如基准测试、绕过类型别名违规、集成汇编代码或信号处理(仅在同一线程中处理的信号)。

1
在第二个例子中,即使我们假设没有任何重新排序,行为也是未定义的。变量flag的写入和读取不是原子性的,存在竞争条件1。没有重新排序并不能保证两个线程不同时访问变量flag。当一个线程在函数readflag的while循环中命中并读取flag时,另一个线程在writeflag中写入flag,就会发生这种情况。

1(引自:ISO/IEC 14882:2011(E) 1.10多线程执行和数据竞争21)
如果程序在不同的线程中包含两个冲突操作,其中至少一个不是原子操作,并且它们之间没有先后顺序,则程序的执行将包含数据竞争。任何此类数据竞争都会导致未定义的行为。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接