使用C / Pthread:共享变量是否需要可变?

时间:2020-03-05 18:57:27  来源:igfitidea点击:

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

pthread POSIX标准对此是否有任何说法,是否依赖于编译器?

编辑添加:感谢出色回答。但是,如果我们不使用锁怎么办?例如,如果我们使用障碍物怎么办?或者使用比较和交换之类的原语直接和原子地修改共享变量的代码...

解决方案

回答

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

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

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

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

回答

根据我的经验,不可以;我们只需要在写入这些值时适当地使自己静音,或者构造程序以使线程在它们需要访问依赖于另一个线程的操作的数据之前停止运行。我的项目x264使用这种方法。线程共享大量数据,但其中绝大多数不需要互斥体,因为其只读或者线程将等待数据变得可用并最终确定后才需要访问它。

现在,如果我们有许多线程在它们的操作中完全交织在一起(它们在非常精细的级别上依赖于彼此的输出),那么这可能会困难得多-实际上,在这种情况下,我会考虑重新审视线程模型,以查看是否可以更干净地完成线程之间的分隔。

回答

只要我们使用锁来控制对变量的访问,就不需要在其上使用volatile。实际上,如果将volatile放在任何变量上,则可能已经错了。

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

回答

仅当我们绝对不需要在一个线程写入内容与另一个线程读取内容之间存在任何延迟时,Volatile才有用。但是,如果没有某种类型的锁,我们将不知道另一个线程何时写入数据,仅知道它是最新的可能值。

对于简单的值(int和float各种大小),如果不需要显式的同步点,则互斥可能会过大。如果我们不使用某种互斥或者锁,则应将变量声明为volatile。如果我们使用互斥锁,则一切准备就绪。

对于复杂的类型,必须使用互斥锁。对它们的操作是非原子的,因此我们可以阅读没有互斥量的半修改版本。

回答

我认为volatile的一个非常重要的特性是,它使变量在修改后写入内存,并在每次访问时从内存中重新读取。这里的其他答案混合了volatile和同步,从其他答案中可以明显看出,volatile不是同步原语(应归功于信用)。

但是,除非我们使用volatile,否则编译器可以自由地在任何时间长度内将共享数据缓存在寄存器中……如果我们希望将数据写入到可预测的实际内存中,而不仅仅是缓存编译器自行决定,我们将需要将其标记为易失性。或者,如果仅在留下修改功能后才访问共享数据,则可能会很好。但是我建议不要依靠盲目运气来确保将值从寄存器写回内存。

尤其是在寄存器丰富的机器(即非x86)上,变量可以在寄存器中生存很长时间,而好的编译器甚至可以将结构的一部分或者整个结构缓存在寄存器中。因此,我们应该使用volatile,但为了提高性能,还应将值复制到局部变量以进行计算,然后进行显式写回。本质上,有效地使用volatile意味着在C代码中进行一些负载存储思考。

无论如何,我们肯定必须使用某种操作系统级提供的同步机制来创建正确的程序。

有关volatile弱点的示例,请参见http://jakob.engbloms.se/archives/65上我的Decker算法示例,该示例很好地证明了volatile无法同步。

回答

不。

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

volatile的主要用法是用于访问内存映射的I / O。在这种情况下,基础设备可以独立于CPU更改内存位置的值。如果在这种情况下不使用" volatile",则CPU可能会使用以前缓存的内存值,而不是读取新更新的值。

回答

答案是绝对的,毫无疑问的。除了适当的同步原语之外,我们不需要使用" volatile"。这些原语完成了所有需要完成的工作。

使用" volatile"既不是必需的也不是足够的。这是没有必要的,因为适当的同步原语就足够了。这还不够,因为它只会禁用某些优化,而不是所有可能会咬住优化。例如,它不能保证在另一个CPU上的原子性或者可见性。

But unless you use volatile, the compiler is free to cache the shared data in a register for any length of time... if you want your data to be written to be predictably written to actual memory and not just cached in a register by the compiler at its discretion, you will need to mark it as volatile. Alternatively, if you only access the shared data after you have left a function modifying it, you might be fine. But I would suggest not relying on blind luck to make sure that values are written back from registers to memory.

是的,但是即使我们确实使用了volatile,CPU也可以在任何时间范围内自由地将共享数据缓存在写发布缓冲区中。可能会困扰优化设置与" volatile"禁用的优化设置并不完全相同。因此,如果我们使用"易失性",那么我们将依靠盲目的运气。

另一方面,如果将同步原语与已定义的多线程语义一起使用,则可以确保一切正常。另外,我们不会承受" volatile"带来的巨大性能损失。那么为什么不那样做呢?

回答

我不明白
同步原语如何迫使编译器重新加载变量的值?
为什么不使用现有的最新副本呢?

易失性意味着该变量在代码范围之外进行了更新,因此,编译器无法假定其知道其当前值。
甚至内存屏障也没有用,因为忽略了内存屏障的编译器(对吗?)可能仍使用缓存的值。

回答

显然有人认为编译器将同步调用视为内存障碍。 " Casey"假设恰好有一个CPU。

如果同步原语是外部函数,并且所讨论的符号在编译单元之外可见(全局名称,导出的指针,可以修改它们的导出函数),则编译器会将它们(或者任何其他外部函数调用)视为内存屏障关于所有外部可见物体。

否则,我们将独自一人。而volatile可能是使编译器生成正确,快速代码的最佳工具。但是,当我们需要volatile时,它通常不是可移植的,而它实际为我们所做的工作很大程度上取决于系统和编译器。

回答

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