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

时间:2016-06-07 15:28:05

标签: c++ multithreading c++11 gcc

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

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

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

我在does-pthread-mutex-lock-have-happens-before-semantics中找到了一个可能的答案,表示mail表示外部函数调用充当编译器内存栅栏。

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

4 个答案:

答案 0 :(得分:14)

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

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

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

操作系统的工作是确保其API提供的并发锁实际正常工作 - std::thread旨在成为这种原语的跨平台接口。

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

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

pthread_lock_mutex();
do_some_stuff();
pthread_unlock_mutex();

pthread_lock_mutexpthread_unlock_mutex对应于您平台上的lockunlock的实现,以及惯用的C ++ 11代码,例如,如果您使用std::mutexctor dtor,则会调用这些内容。)

通常,优化程序无法重新排序这些,除非确定std::unique_lock没有可能改变pthread_lock_mutex()的可观察行为的副作用。

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

如果有资源

do_some_stuff()

在各种线程之间存在争用,这意味着存在一些函数体

int resource;

和一个函数指针是在程序中传递给void compete_for_resource(); 的某个早期点,以便启动另一个线程。 (这可能是pthread_create... ctor的实现。)此时,编译器可以看到对std::thread的任何调用都可能调用libpthread并触摸该功能触及的任何记忆。 (从编译器的角度来看compete_for_resource是一个黑盒子 - 它是一些libpthread / .dll,它不能对它究竟是什么做出假设。)

特别是,调用.so可能会对pthread_lock_mutex();产生副作用,因此无法针对resource进行重新排序。

如果你从未真正产生任何其他线程,那么据我所知,do_some_stuff()可以在互斥锁之外重新排序。由于do_some_stuff();无权访问libpthread,因此它只是源代码中的私有变量,并且即使间接也与外部库共享,并且编译器可以看到。

答案 1 :(得分:3)

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

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

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

对于互斥锁,即使是这样的高级优化也无法进行。重新排序互斥锁定/解锁函数调用的唯一方法是深入检查函数,并证明没有障碍或原子操作要处理。如果它无法检查该锁定/解锁功能的每个子呼叫和子子呼叫,则无法证明重新排序是安全的。如果确实可以进行此检查,则会看到每个互斥锁实现包含无法重新排序的内容(实际上,这是有效互斥实现的定义的一部分)。因此,即使在极端情况下,仍然禁止编译器进行优化。

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

为了解决这个问题,像pthreads这样的多线程API开发了自己的规则。来自Pthreads specification section 4.11

  

应用程序应确保更多地访问任何内存位置   比一个控制线程(线程或进程)受到限制   没有控制线程可以读取或修改内存位置   另一个控制线程可能正在修改它。这种访问是   限制使用同步线程执行的函数   使内存与其他线程同步。下列   函数使内存与其他线程同步

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

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

在gcc的情况下,它没有任何关于pthreads函数调用的特殊注释,因为gcc会有效地在每个外部函数调用周围创建一个屏障(因为它无法证明没有同步存在于该函数调用中)。如果gcc要改变它,他们还必须改变他们的pthreads头,以包括将pthreads函数标记为同步内存所需的任何额外的verbage。

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

答案 2 :(得分:2)

注意:我不是这个领域的专家,而且我对它的了解是在意大利面条状。所以拿一粒盐给出答案。

注2:这可能不是OP期望的答案。但如果有帮助的话,这是我的2美分:

  

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

g ++使用pthread库。 std :: mutex只是pthread_mutex的一个薄包装器。所以,你必须真正去看看pthread的互斥实现 如果你深入了解pthread实现(你可以找到here),你会发现它使用原子指令和futex调用。

这里要记住两件小事:
原子指令确实使用了障碍 2.任何函数调用都相当于完全屏障。不记得从哪里读到它。
3. mutex调用可能会使线程进入休眠状态并导致上下文切换
现在,就重新排序而言,需要保证的一点是,lock之后和unlock之前的任何指令都不应该重新排序到lock之前或之后{{1 }}。我认为这不是一个完全障碍,而是分别只是获得和释放障碍。但是,这又是依赖于平台的,x86默认提供顺序一致性,而ARM则提供较弱的排序保证

我强烈推荐这个博客系列: http://preshing.com/archives/ 它用易于理解的语言解释了许多低级的东西。猜猜,我必须再读一遍:)

UPDATE ::由于长度无法评论@Cort Ammons的回答

@Kane我不确定这一点,但是人们通常会为处理器级别设置障碍,这也会影响编译器级别的障碍。编译器内置障碍也是如此。

现在,由于unlock函数定义不存在于您正在使用它的翻译单元中(这是值得怀疑的),调用lock-unlock应该为您提供完整的内存屏障。该平台的pthread实现利用原子指令阻止任何其他线程在锁定之后或解锁之前访问存储器位置。现在,因为只有一个线程正在执行代码的关键部分,所以确保其中的任何重新排序都不会改变上述注释中提到的预期行为。

Atomics非常难以理解和正确,所以,我上面写的是我的理解。我很高兴知道我的理解是不是错了。

答案 3 :(得分:0)

  

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

根据定义,任何阻止CPU通过推测执行重新排序的东西都会阻止编译器重新排序。这是语言语义的定义,即使没有语言中的MT(多线程),因此您可以安全地重新排序不支持MT的旧编译器。

但是由于缺乏围绕静态变量的运行时初始化的线程保护到隐式修改的全局变量(如errno等),这些编译器对MT来说不安全。

另外,在C / C ++中,对纯粹外部的函数的任何调用(即:不是内联的,可用于任何点的内联),没有注释解释它的作用(如"纯函数&# 34;某些流行编译器的属性),必须假定做任何合法的C / C ++代码都可以做的事情。不可能进行非平凡的重新排序(任何可见的重新排序都是非常重要的)。

在具有多个执行单元的系统上正确执行锁定,这些锁定不会模拟装配指令上的全局顺序,这将需要内存屏障并阻止重新排序。

线性执行CPU上的锁的实现,只有一个执行单元(或者所有线程都绑定在同一执行单元上),可能只使用volatile变量进行同步,并且因为volatile读取而不安全。写不提供任何获得的保证。发布任何其他数据(对比Java)。需要某种编译器障碍,如强外部函数调用,或某些asm (""/*nothing*/)(特定于编译器,甚至特定于编译器版本)。