编译器内存屏障和互斥锁

11

POSIX标准规定像互斥锁这样的东西会强制进行内存同步。然而,编译器可能会重新排序内存访问。假设我们有

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

编译器重新排序后,它可能会被改变为以下代码,对吗?

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

那么互斥锁如何同步内存访问呢?更准确地说,编译器如何知道在锁定/解锁之间不应该进行重排?

实际上,在单线程方面,就就绪赋值重排而言是完全安全的,因为就绪在函数调用lock(mutex)中没有使用。

编辑后:如果函数调用是编译器无法跨越的内容,那么我们可以将其视为类似于编译器内存屏障的东西吗?

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

你有这样一个重新排序发生的示例吗?由于lock()unlock()只是编译器的函数调用,我怀疑这样的重新排序会发生。 - Andreas Fester
不是的。我只是在想... - user1192878
2个回答

8
一般来说,如果您想用编译器为 POSIX 目标系统编写代码,则需要确保您的编译器支持 POSIX,并且该支持应该知道如何避免在锁定和解锁过程中重新排序。
话虽如此,这种知识通常可以通过一种简单的方式获得:编译器不会重新排序对(非可证明本地)数据的访问,以避免调用使用或修改它们的外部函数。编译器应该对lockunlock 有所了解才能进行重新排序。
并且,不是简单地“调用全局函数总是编译器屏障”,我们应该添加“除非编译器知道有关该函数的特定信息”。这确实发生了:例如,在 Linux(NPTL)上声明了具有 __const__ 属性的 pthread_self,使得 gcc 可以在 pthread_self() 调用之间重新排序,甚至可以完全消除不必要的调用。
我们可以轻松地想象编译器支持获取/释放语义的函数属性,这使得 lockunlock 不再是一个完整的编译器屏障。

POSIX 是一个 API 定义。就编译器而言,除了使特定函数库可用之外,没有其他支持。也就是说……像优化(其中包括内存访问重排序)这样的编译器主题与 POSIX 完全无关。 - mah
4
请参考戴夫·布滕霍夫(Dave Butenhof)在这篇帖子中对POSIX与编译器关系的看法。(http://newsgroups.derkeiler.com/Archive/Comp/comp.programming.threads/2007-11/msg00006.html) - Anton Kovalenko
哇..这篇帖子正好解答了我的问题!并且解释得非常好! - user1192878

3

编译器不会在不确定安全的情况下重排代码。在你的"what if"例子中,你并没有提出重新排列内存访问的问题,而是在询问编译器完全更改代码顺序的情况--但实际上编译器不会这么做。编译器可能会更改实际内存读/写的顺序,但不会更改函数调用的顺序(无论是否涉及到这些内存访问)。

一个编译器可能会重排内存访问的例子...假设您有以下代码:

a = *pAddressA;
b = *pAddressB;

现在假设pAddressB的值在寄存器中,而pAddressA的值不在。编译器可以先读取地址B的值,然后将pAddressA的值移动到同一个寄存器中,以便接收新位置。如果在这些访问之间存在函数调用,则编译器不能这样做。


实际上,这里只涉及单线程方面,因为在函数调用lock(mutex)中没有使用ready,所以准备好的赋值重排是完全安全的。 - user1192878
@user1192878 编译器并不总能确定函数调用不持有更改变量地址的意图,因此编译器不能将您建议的行为视为安全(作为概括性陈述)。编译器通常无法跨函数进行优化(在不同源文件中),这是您的建议所要求的。 - mah
@user1192878 我们没有被告知 ready 是全局变量还是局部变量,也没有被告知它是否是一个 volatile 变量。如果 lock()unlock() 在一个单独的编译单元中,特别是如果它们是在一个单独的文件中以汇编代码实现的话,编译器可能不知道它们内部的情况。 - Alexey Frunze

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