GCC之类的编译器如何为std :: mutex实现获取/释放语义

SSY*_*SSY 21 c++ multithreading gcc c++11

我的理解是std :: mutex锁定和解锁具有获取/释放语义,这将防止它们之间的指令被移到外面.

因此,获取/释放应禁用编译器和CPU重新排序指令.

我的问题是我看一下GCC5.1代码库,并没有看到std :: mutex :: lock/unlock中的任何特殊内容,以防止编译器重新排序代码.

我在do-pthread-mutex-lock-have-happen-before-semantics中找到了一个潜在的答案,表示一个外部函数调用充当编译器内存栅栏的邮件.

它总是如此吗?标准在哪里?

Chr*_*eck 14

线程是一个相当复杂的低级功能.从历史上看,没有标准的C线程功能,而是在不同的操作系统上以不同的方式完成.今天主要有POSIX线程标准,已在Linux和BSD中实现,现在通过扩展OS X,并且有Windows线程,从Win32开始.除了这些之外,可能还有其他系统.

GCC不直接包含POSIX线程实现,而是可能是libpthreadLinux系统上的客户端.当您从源代码构建GCC时,您必须单独配置和构建许多辅助库,支持大数字和线程等内容.这就是您选择如何完成线程的点.如果你在linux上采用标准方式,你就可以实现std::threadpthreads.

在Windows上,从MSVC C++ 11合规性开始,MSVC开发人员std::thread在本机Windows线程接口方面实现.

操作系统的工作是确保其API提供的并发锁实际工作 - std::thread意味着是这种原语的跨平台接口.

对于更奇特的平台/交叉编译等情况可能更复杂.例如,在MinGW项目(gcc for windows)中 - 历史上,您可以选择使用pthreads到Windows的端口来构建MinGW gcc,或者使用基于win32的本机线程模型.如果在构建时没有配置它,最终可能会得到一个不支持std::thread或不支持的C++ 11编译器std::mutex.有关详细信息,请参阅此问题.MinGW错误:'thread'不是'std'的成员

现在,更直接地回答你的问题.当互斥锁处于最低级别时,这涉及对libpthreads或某些win32 API的调用.

pthread_lock_mutex();
do_some_stuff();
pthread_unlock_mutex();
Run Code Online (Sandbox Code Playgroud)

(在pthread_lock_mutexpthread_unlock_mutex对应的实现lockunlockstd::mutex你的平台上,并在地道的C++代码11,这些反过来又堪称ctordtorstd::unique_lock,如果你正在使用的实例.)

通常,优化器不能重新排序这些,除非它确定pthread_lock_mutex()没有可能改变其可观察行为的副作用do_some_stuff().

据我所知,编译器执行此操作的机制最终与用于估计调用任何其他外部库的潜在副作用的机制相同.

如果有一些资源

int resource;
Run Code Online (Sandbox Code Playgroud)

这是各种线程之间的争论,它意味着有一些功能体

void compete_for_resource();
Run Code Online (Sandbox Code Playgroud)

并且一个函数指针指向此程序的某个更早点pthread_create...,以便启动另一个线程.(这大概是在实施ctorstd::thread.)在这一点上,编译器可以看到,任何调用到libpthread可以潜在地调用compete_for_resource和触摸任何记忆这项职能触及.(从编译器的角度来看,它libpthread是一个黑盒子 - 它是一些.dll/ .so它无法对它到底做什么做出假设.)

特别是,该调用pthread_lock_mutex();可能具有副作用resource,因此无法对其进行重新排序do_some_stuff().

如果你从未真正产生任何其他线程,那么据我所知,do_some_stuff();可以在互斥锁之外重新排序.因为,然后libpthread没有任何访问权限resource,它只是源代码中的私有变量,甚至没有间接与外部库共享,编译器可以看到.

  • _没有标准的C线程功能_那[不再是真的](http://en.cppreference.com/w/c/thread). (3认同)

Cor*_*ica 5

所有这些问题都源于编译器重新排序的规则。重新排序的基本规则之一是编译器必须证明重新排序不会改变程序的结果。在 的情况下std::mutex,该短语的确切含义是在大约 10的法律术语中指定的,但“不会改变程序的结果”的一般直观感觉仍然成立。如果您保证哪个操作先发生,根据规范,不允许编译器以违反该保证的方式重新排序。

这就是为什么人们经常声称“函数调用充当内存屏障”。如果编译器无法深入检查该函数,则无法证明该函数内部没有隐藏的屏障或原子操作,因此它必须将该函数视为屏障。

当然,在某些情况下编译器可以检查函数,例如内联函数或链接时间优化的情况。在这些情况下,不能依赖函数调用来充当屏障,因为编译器可能确实有足够的信息来证明重写的行为与原始行为相同。

在互斥锁的情况下,甚至无法进行这种高级优化。围绕互斥锁/解锁函数调用重新排序的唯一方法是深入检查函数并证明不存在需要处理的障碍或原子操作。如果它无法检查该锁定/解锁函数的每个子调用和子子调用,则无法证明重新排序是安全的。如果它确实可以进行此检查,它将发现每个互斥体实现都包含无法重新排序的内容(实际上,这是有效互斥体实现的定义的一部分)。因此,即使在这种极端情况下,编译器仍然被禁止进行优化。

编辑:为了完整起见,我想指出这些规则是在 C++11 中引入的。C++98 和 C++03 重新排序规则仅禁止影响当前线程结果的更改。这样的保证不足以开发像互斥体这样的多线程原语。

为了解决这个问题,pthreads 等多线程 API 开发了自己的规则。来自Pthreads 规范第 4.11 节

应用程序应确保限制多个控制线程(线程或进程)对任何内存位置的访问,以便在另一个控制线程可以修改该内存位置时,任何控制线程都无法读取或修改该内存位置。使用同步线程执行以及相对于其他线程同步内存的函数来限制此类访问。以下函数相对于其他线程同步内存

然后它列出了几十个同步内存的函数,包括pthread_mutex_lockpthread_mutex_unlock

希望支持 pthreads 库的编译器必须实现一些东西来支持这种跨线程内存同步,即使 C++ 规范对此没有任何说明。幸运的是,任何您想要执行多线程的编译器都是在开发时认识到这种保证是所有多线程的基础,因此每个支持多线程的编译器都有它!

对于 gcc,它这样做时没有对 pthread 函数调用进行任何特殊注释,因为 gcc 会有效地在每个外部函数调用周围创建一个屏障(因为它无法证明该函数调用内部不存在同步)。如果 gcc 要改变这一点,他们还必须更改其 pthreads 标头,以包含将 pthreads 函数标记为同步内存所需的任何额外措辞。

当然,所有这些都是特定于编译器的。在 C++11 及其新内存模型出现之前,这个问题没有标准答案。

  • “重新排序的基本规则之一是编译器必须证明重新排序不会改变程序的结果。”这只是部分正确。编译器做出的唯一保证是程序在从“单线程”运行时能够正确运行。 (2认同)
  • @Arunmu 你所说的对于 C++11 之前的 C++ 来说是正确的。然而,Kane 给 C++11 打了标签,并引用了 `std::mutex`,它只存在于 C++11 及之后的版本中。在 C++11 中,有关重新排序的规则进行了大幅修改以支持多线程。在此之前,多线程设置中的任何排序保证都是“特定于编译器的”。像 pthreads 这样的库确实依赖于编译器特定的功能保证。Pthreads 有一个措辞,即某些函数“同步”,这限制了重新排序,如果编译器希望使用 pthreads,则必须支持该措辞。 (2认同)