问题 编译器内存屏障和互斥锁


posix标准说像互斥体这样的东西会强制执行内存同步。 但是,编译器可能会重新排序内存访问。 说我们有

lock(mutex);
setdata(0);
ready = 1;
unlock(mutex);

可能会通过编译器重新排序将其更改为下面的代码,对吗?

ready = 1;
lock(mutex);
setdata(0);
unlock(mutex);

那么互斥量如何同步内存访问?更确切地说,编译器如何知道重锁不应该在锁定/解锁时发生?

实际上这里对于单线程方面,就绪分配重新排序是完全安全的,因为就绪不用于函数调用锁(互斥锁)。

编辑: 因此,如果函数调用是编译器无法实现的, 我们可以将其视为编译器内存屏障

asm volatile("" ::: "memory")

8660
2018-01-21 11:44


起源

你有这样一个重新排序的样本吗?以来 lock() 和 unlock() 只是函数调用编译器,我怀疑这样的重新排序会发生 - Andreas Fester
实际上没有。我只是想知道... - user1192878


答案:


一般的答案是,如果你想将它用于POSIX目标,你的编译器应该支持POSIX,而这种支持意味着它应该知道避免重新排序锁定和解锁。

也就是说,这种知识通常以一种微不足道的方式实现:编译器不会通过调用可能使用或修改它们的外部函数来重新排序对(不可证实的本地)数据的访问。应该知道一些事情 特别 关于 lock 和 unlock 能够重新排序。

不,这并不是那么简单,因为“对全局函数的调用始终是编译器障碍” - 我们 应该 添加“除非编译器知道有关该函数的特定内容”。它真的发生了:例如 pthread_self 在Linux(NPTL)上声明 __const__ 属性,允许 gcc 重新排序 pthread_self() 电话,甚至完全消除不必要的电话。

我们很容易 想像 一个支持获取/释放语义的函数属性的编译器 lock 和 unlock 不到一个 充分 编译屏障。


8
2018-01-21 11:53



POSIX 是一个API定义。就编译器而言,除了使特定的函数库可用之外,没有任何东西可以支持。那就是......优化(内存访问重新排序)等编译器tpoics与POSIX完全无关。 - mah
@mah看 这个帖子 关于Dave Butenhof对POSIX与编译器关系的看法。 - Anton Kovalenko
哇..这些帖子正是关于我的问题!和很好的解释! - user1192878


编译器不会在不清楚它是否安全的情况下重新排序。在你的“假设”示例中,你没有提出重新排序的内存访问,你问的是编译器是否完全改变了代码排序 - 而且它不会。编译器可能做的事情是改变实际内存读/写的顺序,但不改变函数调用(有或没有那些内存访问)。

编译器可能重新排序内存访问的示例...假设您有以下代码:

a = *pAddressA;
b = *pAddressB;

并让我们考虑一下其价值的情况 pAddressB 是在注册的同时 pAddressA 不是。编译器首先读取地址B,然后移动值的公平游戏 pAddressA 进入同一个寄存器,以便可以接收新的位置。如果在这些访问之间碰巧发生了函数调用,则编译器无法执行此操作。


3
2018-01-21 11:51



实际上这里对于单线程方面,就绪分配重新排序是完全安全的,因为就绪不用于函数调用锁(互斥锁)。 - user1192878
@ user1192878编译器不能总是确定函数调用没有保存变量的地址而有意改变它,因此编译器不能考虑你建议的安全性(作为一揽子声明)。编译器通常不能跨函数(在不同的源文件中)进行优化,这是您的建议所需要的。 - mah
@ user1192878我们没有被告知是否 ready 是一个全局变量或局部变量,无论它是否是a volatile 变量。编译器可能不知道里面是什么 lock() 和 unlock(),如果它们位于单独的编译单元中,特别是如果它们在单独的文件中实现为汇编代码。 - Alexey Frunze
是的。我想你是对的。谢谢! - user1192878