互斥锁定和解锁功能如何防止 CPU 重新排序?

How does a mutex lock and unlock functions prevents CPU reordering?

据我所知,函数调用充当编译器障碍,但不是 CPU 障碍。

这个 tutorial 表示如下:

acquiring a lock implies acquire semantics, while releasing a lock implies release semantics! All the memory operations in between are contained inside a nice little barrier sandwich, preventing any undesireable memory reordering across the boundaries.

我认为上面的引用是在谈论 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 imov 20 into j 重新排序到 call pthread_mutex_lock() 以上或 call pthread_mutex_unlock() 以下?

如果是 call 指令阻止了 CPU 进行重新排序,那么为什么我引用的教程看起来像是互斥锁定和解锁功能阻止了 CPU CPU 重新排序,为什么我引用的教程没有说任何函数调用都会阻止 CPU 重新排序?

我的问题是关于 x86 架构的。

如果ij是局部变量,什么都没有。如果编译器可以证明当前函数之外的任何内容都没有它们的地址,则编译器可以将它们保存在函数调用的寄存器中。

但是任何全局变量,或者其地址可能存储在全局中的局部变量,do 必须在内存中 "in sync" - 内联函数调用。 编译器必须假设它不能内联的任何函数调用都会修改它可能引用的任何/每个变量。

因此,例如,如果 int i; 是一个局部变量,在 sscanf("0", "%d", &i); 之后它是 address will have escaped the function 并且编译器将不得不 spill/reload 它围绕函数调用而不是将其保存在调用保留寄存器中。

请参阅我在 上的回答,例如 asm volatile("":::"memory") 是其地址转义函数 (sscanf("0", "%d", &i);) 的局部变量的障碍,但不是针对那些还是纯本地的。出于完全相同的原因,这是完全相同的行为。


I assume that the above quote is talking about CPU reordering and not about compiler reordering.

它正在谈论两者,因为两者都是正确性所必需的。

这就是为什么 编译器 无法使用 any 函数调用对共享变量的更新重新排序。 (这很重要:弱 C11 内存模型允许大量 compile-time reordering。强 x86 内存模型只允许 StoreLoad 重新排序和本地存储转发。)

pthread_mutex_lock 作为非内联函数调用负责编译时重新排序,以及它执行 locked 操作的事实,一个原子 RMW,也意味着它在 x86 上包含一个完整的 运行time 内存屏障。 (不过,不是 call 指令本身,只是函数体中的代码。)这赋予它获取语义。

解锁自旋锁只需要释放存储,而不是 RMW,因此根据实现细节,解锁功能可能不是 StoreLoad 障碍。 (这仍然没问题:它可以防止临界区中的所有内容被泄露。没有必要阻止后续操作在解锁之前出现。参见 Jeff Preshing 的 article explaining Acquire and Release semantics

在弱顺序 ISA 上,这些互斥函数将 运行 屏障指令,如 ARM dmb(数据内存屏障)。普通函数不会,所以该指南的作者指出这些函数是特殊的是正确的。


Now what prevents the CPU from reordering mov 10 into i and mov 20 into j to above call pthread_mutex_lock()

这不是重要原因(因为在弱序 ISA 上 pthread_mutex_unlock 会 运行 屏障指令),但 在 x86 上确实如此存储甚至不能用 call 指令 重新排序,更不用说函数体在函数 returns.[=34 之前完成的互斥锁的实际 locking/unlocking =]

x86 具有强大的内存排序语义(存储不与其他存储重新排序),call 是一个存储(推送 return 地址)。

所以mov [i], 10必须出现在call指令完成的存储之间的全局存储中。

当然在正常的程序中,没有人在观察其他线程的调用堆栈,只是 xchg 获取互斥锁或 release-store 在 pthread_mutex_unlock 中释放它。

简短的回答是 pthread_mutex_lockpthread_mutex_unlock 调用的主体将包含必要的特定于平台的内存屏障,这将防止 CPU 在它外面的关键部分。指令流将通过 call 指令从调用代码移动到 lockunlock 函数,就是这个 dynamic instruction trace 你必须考虑重新排序的目的 - 而不是您在汇编列表中看到的静态序列。

特别是在 x86 上,您可能不会在这些方法中找到 明确的、独立的 内存屏障,因为您已经有 lock-prefixed 指令来执行实际的原子锁定和解锁,这些指令 暗示 一个完整的内存屏障,它可以防止 CPU 您担心的重新排序。

例如,在我的带有 glibc 2.23 的 Ubuntu 16.04 系统上,pthread_mutex_lock 是使用 lock cmpxchg(比较和交换)实现的,pthread_mutex_unlock 是实现的使用 lock dec(递减),两者都具有完整的屏障语义。