据我所知,函数调用充当编译器屏障,而不是CPU屏障。
此tutorial说:
获取锁意味着在释放锁的同时获取语义 暗示释放语义!两者之间的所有内存操作都是 包含在一个很好的小屏障三明治中,可以防止任何 不必要的内存重新排序。
我认为以上引用是关于CPU重新排序,而不是关于编译器重新排序。
但是我不明白互斥锁的锁定和解锁如何使CPU赋予这些功能获取和释放语义。
例如,如果我们具有以下C代码:
pthread_mutex_lock(&lock);
i = 10;
j = 20;
pthread_mutex_unlock(&lock);
以上C代码被翻译为以下(伪)汇编指令:
push the address of lock into the stack
call pthread_mutex_lock()
mov 10 into i
mov 20 into j
push the address of lock into the stack
call pthread_mutex_unlock()
现在是什么阻止CPU将mov 10 into i
和mov 20 into j
重新排序到call pthread_mutex_lock()
以上或call pthread_mutex_unlock()
以下?
如果是call
指令阻止了CPU进行重新排序,那么为什么我引用的教程使它看起来像是互斥锁和解锁函数阻止了CPU的重新排序,为什么该教程我引用的意思不是说任何函数调用都会阻止CPU重新排序?
我的问题是关于x86架构的。
答案 0 :(得分:5)
简短的回答是,pthread_mutex_lock
和pthread_mutex_unlock
调用的主体将包含必要的特定于平台的内存屏障,这将阻止CPU将内存访问移至其外部的关键部分。指令流将通过lock
指令从调用代码移至unlock
和call
函数,这是您必须考虑的动态指令跟踪出于重新排序的目的-而不是在装配清单中看到的静态序列。
特别是在x86上,您可能不会在这些方法内找到显式,独立内存屏障,因为您已经拥有lock
-prefixed指令以执行实际的锁定和解锁从原子上讲,这些指令暗含一个完整的内存屏障,可防止您担心的CPU重新排序。
例如,在具有glibc 2.23的Ubuntu 16.04系统上,pthread_mutex_lock
使用lock cmpxchg
(比较并交换)实现,而pthread_mutex_unlock
使用lock dec
实现(递减),两者都具有完全的障碍语义。
答案 1 :(得分:4)
如果i
和j
是局部变量,则什么都没有。如果编译器可以证明当前函数之外的任何东西都没有其地址,则编译器可以将它们保存在函数调用的寄存器中。
但是任何全局变量或地址可能存储在全局变量中的局部变量,都必须在内存中“同步”才能进行非内联函数调用。编译器必须假定它无法内联的任何函数调用都会修改其可能引用的任何变量。
例如,如果int i;
是局部变量,则在scanf("0", "%d", &i);
之后,其地址将转义该函数,然后编译器将不得不在函数调用周围溢出/重新加载它,而不是将其保留在保留呼叫的寄存器。
请参阅我对Understanding volatile asm vs volatile variable的回答,以asm volatile("":::"memory")
为例,该变量对地址转义了函数(scanf("0", "%d", &i);
)的局部变量是一个障碍,但对于纯粹的局部变量则不是本地。出于完全相同的原因,这是完全相同的行为。
我认为以上引用是关于CPU重新排序,而不是关于编译器重新排序。
它在谈论两者,因为两者对于正确性都是必需的。
这就是为什么编译器不能通过 any 函数调用对共享变量的更新进行重新排序的原因。 (这非常重要:弱的C11内存模型允许大量compile-time reordering。强的x86内存模型仅允许StoreLoad重新排序和本地存储转发。)
pthread_mutex_lock
是一个非内联函数调用,它负责编译时重新排序,并且它执行lock
ed操作(原子RMW),表示它在x86上包含完整的运行时内存屏障。 (不过,call
指令本身并不是函数体内的代码。)这使它获得了语义。
解锁自旋锁仅需要一个发布存储,而不需要RMW,因此根据实现细节,解锁功能可能不是StoreLoad障碍。 (这还是可以的:它使关键部分的所有内容都不会消失。不必在解锁之前阻止以后的操作出现。请参阅Jeff Preshing的article explaining Acquire and Release semantics)
在顺序较弱的ISA上,这些互斥函数将运行屏障指令,例如ARM dmb
(数据存储器屏障)。普通功能不会,因此该指南的作者正确指出那些功能是特殊的。
现在,是什么原因阻止了 CPU 将mov 10重新排列为i,并将mov 20重新排列为j到
以上call pthread_mutex_lock()
这不是重要的原因(因为在顺序较弱的ISA pthread_mutex_unlock
上会执行屏障指令),但是在x86上实际上是正确的,甚至无法对商店进行重新排序call
指令,更不用说在函数返回之前由函数体完成的互斥锁的实际锁定/解锁。
x86具有很强的内存排序语义(商店不会与其他商店重新排序),而call
是商店(推送返回地址)。
因此mov [i], 10
必须出现在call
指令完成的存储之间的全局存储中。
当然,在普通程序中,没有人观察到其他线程的调用堆栈,只是观察xchg
来获取互斥量,或者观察到释放存储区来将其释放到pthread_mutex_unlock
中。