使用C / Pthreads:共享变量需要是不稳定的吗?

在C编程语言中,Pthreads作为线程库; 在线程之间共享的变量/结构是否需要声明为volatile? 假设他们可能受到锁定或不受保护(可能是障碍)。

pthread POSIX标准对此有任何发言权,是依赖于编译器还是不依赖于编译器?

编辑添加:感谢您的答案。 但是,如果你使用锁,那该怎么办? 如果你使用障碍物怎么办? 或者使用比较和交换等原语直接和primefaces地修改共享变量的代码……

我认为volatile的一个非常重要的属性是它在变量时将变量写入内存,并在每次访问时从内存重新读取。 这里的其他答案混合了volatile和同步,并且从其他一些答案可以清楚地看出,volatile不是同步原语(信用到期的信用)。

但除非你使用volatile,否则编译器可以自由地将共享数据缓存在寄存器中任何时间长度……如果你希望将数据写入可预测地写入实际内存而不是仅仅缓存在寄存器中编译器自行决定,您需要将其标记为volatile。 或者,如果您在离开修改它的函数后才访问共享数据,那么您可能没问题。 但我建议不要依赖盲目的运气来确保将值从寄存器写回内存。

特别是在富寄存器的机器上(即不是x86),变量可以在寄存器中存活很长时间,而良好的编译器甚至可以缓存寄存器中的部分结构或整个结构。 所以你应该使用volatile,但是为了性能,还要将值复制到局部变量进行计算,然后进行显式回写。 从本质上讲,有效地使用volatile意味着在C代码中进行一些加载存储思维。

在任何情况下,您都必须使用某种操作系统级别提供的同步机制来创建正确的程序。

有关volatile的弱点的一个例子,请参阅我在http://jakob.engbloms.se/archives/65上的 Decker算法示例,这certificate了volatile不能同步。

只要您使用锁来控制对变量的访问,就不需要使用volatile。 事实上,如果你在任何变量上放置volatile,你可能已经错了。

https://software.intel.com/en-us/blogs/2007/11/30/volatile-almost-useless-for-multi-threaded-programming/

答案是绝对的,毫不含糊的,不。 除了正确的同步原语之外,您不需要使用’volatile’。 需要完成的一切都是由这些原语完成的。

使用’volatile’既不必要也不充分。 没有必要,因为适当的同步原语就足够了。 这还不够,因为它只会禁用一些优化,而不是所有可能会咬你的优化。 例如,它不保证另一个CPU的primefaces性或可见性。

但除非你使用volatile,否则编译器可以自由地将共享数据缓存在寄存器中任何时间长度……如果你希望将数据写入可预测地写入实际内存而不是仅仅缓存在寄存器中编译器自行决定,您需要将其标记为volatile。 或者,如果您在离开修改它的函数后才访问共享数据,那么您可能没问题。 但我建议不要依赖盲目的运气来确保将值从寄存器写回内存。

是的,但即使您使用volatile,CPU也可以在写入缓冲区中将共享数据缓存一段时间。 可以咬你的优化集与’volatile’禁用的优化集不完全相同。 因此,如果你使用’volatile’,你就是依靠盲目的运气。

另一方面,如果您使用具有已定义的multithreading语义的sychronization原语,则可以保证事情能够正常工作。 作为一个优点,你不会受到’波动’的巨大性能打击。 那么为什么不这样做呢?

人们普遍认为关键字volatile对于multithreading编程很有用。

Hans Boehm 指出 ,volatile只有三种便携式用途:

  • volatile可用于在与setjmp相同的范围内标记局部变量,setjmp的值应在longjmp中保留。 目前还不清楚这种用途的哪一部分会减慢,因为如果无法共享有关的局部变量,primefaces性和排序约束就没有效果。 (甚至不清楚通过要求在longjmp中保留所有变量来减慢这些用途的哪一部分,但这是一个单独的问题,在此不予考虑。)
  • 当变量可以被“外部修改”时可以使用volatile ,但实际上修改是由线程本身同步触发的,例如因为底层存储器被映射在多个位置。
  • 易失性 sigatomic_t可用于以受限方式与同一线程中的信号处理程序通信。 人们可以考虑削弱对sigatomic_t案件的要求,但这似乎是违反直觉的。

如果你为了速度而进行multithreading处理,那么减慢代码肯定不是你想要的。 对于multithreading编程,有两个关键问题,volatile经常被错误地认为是:

  • primefaces
  • 内存一致性 ,即另一个线程看到的线程操作的顺序。

让我们首先处理(1)。 易失性不保证primefaces读取或写入。 例如,在大多数现代硬件上,129位结构的易失性读或写不会是primefaces的。 在大多数现代硬件上,32位int的易失性读或写是primefaces的,但volatile与它无关 。 没有波动,它可能是primefaces的。 primefaces性是编译器的心血来潮。 C或C ++标准中没有任何内容表明它必须是primefaces的。

现在考虑问题(2)。 有时,程序员会将volatile视为关闭易失性访问的优化。 这在实践中基本上是正确的。 但这只是易变的访问,而不是非易失性访问。 考虑这个片段:

  volatile int Ready; int Message[100]; void foo( int i ) { Message[i/10] = 42; Ready = 1; } 

它试图在multithreading编程中做一些非常合理的事情:编写一条消息,然后将其发送到另一个线程。 另一个线程将等待Ready变为非零,然后读取Message。 尝试使用gcc 4.0或icc使用“gcc -O2 -S”进行编译。 两者都将首先存储到Ready,因此它可以与i / 10的计算重叠。 重新排序不是编译器错误。 这是一个积极的优化工作。

您可能认为解决方案是将所有内存引用标记为volatile。 那简直太傻了。 正如之前的引言所说,它只会减慢您的代码速度。 最糟糕的是,它可能无法解决问题。 即使编译器没有重新排序引用,硬件也可能。 在此示例中,x86硬件不会对其重新排序。 Itanium(TM)处理器也不会,因为Itanium编译器会为易失性存储插入内存栅栏。 这是一个聪明的Itanium扩展。 但像Power(TM)这样的芯片将重新排序。 您真正需要的是内存栅栏 ,也称为内存屏障 。 内存栅栏可防止跨越栅栏重新排序内存操作,或者在某些情况下,可防止在一个方向上重新排序。易失性与内存栅栏无关。

那么multithreading编程的解决方案是什么? 使用实现primefaces和围栅语义的库或语言扩展。 按预期使用时,库中的操作将插入正确的栅栏。 一些例子:

  • POSIX线程
  • Windows(TM)线程
  • OpenMP的
  • TBB

基于Arch Robison(英特尔)的文章

根据我的经验,没有; 你只需要在写入这些值时正确地互斥自己,或者构造你的程序,使得线程在需要访问依赖于另一个线程的动作的数据之前就会停止。 我的项目x264使用这种方法; 线程共享大量数据,但绝大多数都不需要互斥锁,因为它只读或线程将等待数据变为可用并在需要访问之前完成。

现在,如果你有许multithreading在它们的操作中都是非常交错的(它们在非常细粒度的层次上依赖于彼此的输出),这可能要困难得多 – 事实上,在这种情况下,我会考虑重新审视线程模型,看看是否可以通过线程之间的更多分离来更干净地完成。

没有。

只有在读取可以独立于CPU读/写命令而改变的存储器位置时才需要Volatile 。 在线程化的情况下,CPU完全控制每个线程对内存的读/写,因此编译器可以假设内存是连贯的并优化CPU指令以减少不必要的内存访问。

volatile的主要用途是访问内存映射的I / O. 在这种情况下,底层设备可以独立于CPU更改存储器位置的值。 如果在这种情况下不使用volatile ,则CPU可以使用先前缓存的内存值,而不是读取新更新的值。

只有当一个线程写入某个东西而另一个线程读取它之间绝对没有延迟时,Volatile才有用。 但是,如果没有某种锁定,您不知道其他线程何时写入数据,只知道它是最新的可能值。

对于简单值(int和float各种大小),如果不需要显式同步点,则互斥量可能过大。 如果不使用某种互斥锁或锁,则应声明变量volatile。 如果您使用互斥锁,那么您已经完成了设置。

对于复杂类型,您必须使用互斥锁。 对它们的操作是非primefaces的,因此您可以在没有互斥锁的情况下读取半更改版本。

易失性意味着我们必须转到内存来获取或设置此值。 如果未设置volatile,则编译后的代码可能会长时间将数据存储在寄存器中。

这意味着您应该将线程之间共享的变量标记为volatile,以便您不会遇到一个线程开始修改值但在第二个线程出现之前没有写入其结果并尝试读取值的情况。

Volatile是一个禁用某些优化的编译器提示。 没有它,编译器的输出程序集可能是安全的,但您应始终将其用于共享值。

如果您不使用系统提供的昂贵的线程同步对象,这一点尤为重要 – 例如,您可能拥有一个数据结构,您可以通过一系列primefaces更改使其保持有效。 许多不分配内存的堆栈是此类数据结构的示例,因为您可以向堆栈添加值,然后移动结束指针或在移动结束指针后从堆栈中删除值。 在实现这样的结构时,volatile对于确保primefaces指令实际上是primefaces的至关重要。

根本原因是C语言语义基于单线程抽象机器 。 只要程序在抽象机器上的“可观察行为”保持不变,编译器就可以自行转换程序。 它可以合并相邻或重叠的存储器访问,多次重做存储器访问(例如在寄存器溢出时),或者只是丢弃存储器访问,如果它认为程序的行为在单个线程中执行时不会改变。 因此,正如您可能怀疑的那样,如果程序实际上应该以multithreading方式执行,则行为发生变化。

正如Paul Mckenney在着名的Linux内核文档中指出的那样:

它_must_not_假设编译器将使用不受READ_ONCE()和WRITE_ONCE()保护的内存引用来执行您想要的操作。 如果没有它们,编译器就有权进行各种“创造性”转换,这些转换在COMPILER BARRIER部分中有所介绍。

READ_ONCE()和WRITE_ONCE()被定义为引用变量的易失性转换。 从而:

 int y; int x = READ_ONCE(y); 

相当于:

 int y; int x = *(volatile int *)&y; 

因此,除非您进行“易失性”访问,否则无论您使用何种同步机制,都无法保证访问只发生一次 。 调用外部函数(例如pthread_mutex_lock)可能会强制编译器对全局变量进行内存访问。 但这只有在编译器无法确定外部函数是否更改这些全局变量时才会发生。 采用复杂的过程间分析和链接时间优化的现代编译器使这个技巧变得毫无用处。

总之,您应该标记由多个线程volatile共享的变量,或者使用volatile casts访问它们。


正如Paul McKenney所指出的那样:

当他们讨论你不希望你的孩子知道的优化技术时,我已经看到了他们眼中闪烁的光芒!


但是看看C11 / C ++ 11会发生什么。

我不明白。 同步原语如何强制编译器重新加载变量的值? 为什么不只使用它已有的最新版本?

易失性意味着变量在代码范围之外更新,因此,编译器不能假定它知道它的当前值。 即使是内存障碍也是无用的,因为没有内存障碍的编译器(对吗?)可能仍然使用缓存值。

有些人显然假设编译器将同步调用视为内存屏障。 “Casey”假设只有一个CPU。

如果同步原语是外部函数,并且有问题的符号在编译单元外可见(全局名称,导出的指针,可能修改它们的导出函数),那么编译器会将它们 – 或任何其他外部函数调用 – 视为关于所有外部可见对象的记忆围栏。

否则,你是独立的。 而volatile可能是使编译器生成正确,快速代码的最佳工具。 它通常不会是可移植的,当你需要volatile时它实际上为你做的事情在很大程度上取决于系统和编译器。

没有。

首先,不需要volatile 。 还有许多其他操作可以提供不使用volatile保证的multithreading语义。 这些包括primefaces操作,互斥体等。

第二, volatile不足。 C标准没有为声明为volatile变量提供有关multithreading行为的任何保证。

因此既不必要也不充分,使用它没有多大意义。

一个例外是特定平台(例如Visual Studio),它具有记录的multithreading语义。

线程之间共享的变量应声明为“volatile”。 这告诉编译器当一个线程写入这样的变量时,写入应该是内存(而不是寄存器)。