我的理解是std::mutex锁定和解锁具有获取/释放语义,这将防止它们之间的指令被移出外部。
因此,获取/释放应同时禁用编译器和CPU重新排序指令。
我的问题是,我看一下GCC5.1代码库,在std::mutex::lock/unlock中看不到任何特殊内容,以防止编译器重新排序代码。
我在does-pthread-mutex-lock-have-happens-before-semantics中找到了一个可能的答案,它表示一个mail,其中说一个外部函数调用充当编译器的内存屏障。
总是这样吗?标准在哪里?
最佳答案
所有这些问题都源于编译器重新排序的规则。重新排序的基本规则之一是,编译器必须证明重新排序不会改变程序的结果。在std::mutex
的情况下,该短语的确切含义是在大约10页的laweese块中指定的,但通常的直觉是“不改变程序的结果”。如果您保证先执行哪个操作,则根据规范,不允许编译器以违反该保证的方式重新排序。
这就是为什么人们经常声称“函数调用充当存储障碍”的原因。如果编译器无法深入检查该函数,则无法证明该函数内部没有隐藏的障碍或原子操作,因此它必须将该函数视为障碍。
当然,存在编译器可以检查函数的情况,例如内联函数或链接时间优化的情况。在这些情况下,不能依赖函数调用来充当障碍,因为编译器可能确实有足够的信息来证明重写行为与原始行为相同。
在互斥锁的情况下,即使这样的高级优化也无法进行。在互斥锁/解锁函数调用周围重新排序的唯一方法是对功能进行深入检查,并证明没有障碍或原子操作需要处理。如果它不能检查该锁定/解锁功能的每个子调用和子子调用,则无法证明重新排序是安全的。如果确实可以进行此检查,则将看到每个互斥体实现都包含一些无法重新排序的东西(实际上,这是有效互斥体定义的一部分)。因此,即使在这种极端情况下,仍然禁止编译器进行优化。
编辑:为了完整起见,我想指出这些规则是在C++ 11中引入的。 C++ 98和C++ 03重新排序规则仅禁止影响当前线程结果的更改。这样的保证不足以开发像互斥锁这样的多线程原语。
为了解决这个问题,像pthreads这样的多线程API制定了自己的规则。来自Pthreads specification section 4.11:
然后,它列出了几十个用于同步内存的函数,包括pthread_mutex_lock
和pthread_mutex_unlock
。
希望支持pthreads库的编译器必须实现某种支持这种跨线程内存同步的功能,即使C++规范未对此进行任何说明。幸运的是,要进行多线程处理的任何编译器都是在认识到这种保证是所有多线程基础的基础上开发出来的,因此,每个支持多线程的编译器都具有这种保证!
就gcc而言,它这样做在pthreads函数调用上没有任何特殊说明,因为gcc会在每个外部函数调用周围有效地创建一个障碍(因为它无法证明该函数调用内部不存在同步)。如果gcc要更改它,则它们还必须更改其pthreads header 以包括将pthreads功能标记为同步内存所需的任何额外的措辞。
当然,所有这些都是编译器特定的。在C++ 11附带其新的内存模型之前,没有标准的答案可以解决这个问题。
关于c++ - 像GCC这样的编译器如何实现std::mutex的获取/释放语义,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/37683493/