在C编程语言和Pthreads作为线程库中;线程之间共享的变量/结构是否需要声明为易失性?假设它们可能受到锁的保护(也许没有障碍)。

pthread POSIX 标准对此有什么说法吗?这是否依赖于编译器?或者两者都不依赖?

编辑添加:感谢您的精彩回答。但如果你是 不是 使用锁;如果你正在使用怎么办 障碍 例如?或者使用原语的代码,例如 比较和交换 直接原子地修改共享变量...

有帮助吗?

解决方案

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

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

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

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

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

其他提示

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

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

答案是绝对、明确的:NO。除了适当的同步原语之外,您不需要使用“易失性”。需要做的一切都是由这些原语完成的。

使用“易失性”既不是必要的,也不是充分的。这不是必需的,因为正确的同步原语就足够了。这还不够,因为它仅禁用一些优化,而不是所有可能会影响您的优化。例如,它不保证原子性或在另一个 CPU 上的可见性。

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

是的,但即使您确实使用了易失性,CPU 也可以自由地将共享数据缓存在写入发布缓冲区中任意长度的时间。可能会困扰您的优化集与“易失性”禁用的优化集并不完全相同。所以如果你使用“易失性”,你 依靠盲目的运气。

另一方面,如果您使用具有定义的多线程语义的同步原语,则可以保证一切正常。一个优点是,您不会受到“不稳定”的巨大性能影响。那么为什么不这样做呢?

人们普遍认为关键字 volatile 对于多线程编程很有好处。

汉斯·伯姆 指出 易失性只有三种可移植用途:

  • 易挥发的 可用于标记与 setjmp 相同范围内的局部变量,其值应在 longjmp 中保留。目前尚不清楚此类使用的哪一部分会被减慢,因为如果无法共享相关局部变量,原子性和排序约束就不会产生任何影响。(甚至不清楚通过要求在 longjmp 中保留所有变量会减慢此类使用的哪一部分,但这是一个单独的问题,这里不予考虑。)
  • 易挥发的 当变量可能被“外部修改”,但修改实际上是由线程本身同步触发时,可以使用,例如因为底层内存被映射到多个位置。
  • A 易挥发的 sigatomic_t 可用于以受限方式与同一线程中的信号处理程序进行通信。人们可以考虑削弱 sigatomic_t 情况的要求,但这似乎相当违反直觉。

如果你是 多线程 为了速度,放慢代码速度绝对不是你想要的。对于多线程编程,有两个关键问题常常被错误地认为是需要解决的:

  • 原子性
  • 内存一致性, , IE。另一个线程看到的线程操作的顺序。

我们先来处理(1)。易失性不保证原子读取或写入。例如,在大多数现代硬件上,129 位结构的易失性读取或写入不会是原子的。在大多数现代硬件上,32 位 int 的易失性读取或写入是原子的,但是 挥发性与它无关. 。如果没有易失性,它可能是原子的。原子性是由编译器决定的。C 或 C++ 标准中没有任何内容表明它必须是原子的。

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

 volatile int Ready;       

    int Message[100];      

    void foo( int i ) {      

        Message[i/10] = 42;      

        Ready = 1;      

    }

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

您可能认为解决方案是将所有内存引用标记为易失性。这简直是​​愚蠢至极。正如前面的引述所说,它只会减慢你的代码速度。最糟糕的是,它可能无法解决问题。即使编译器不重新排序引用,硬件也可能会这样做。在此示例中,x86 硬件不会对其进行重新排序。Itanium(TM) 处理器也不会,因为 Itanium 编译器会为易失性存储插入内存栅栏。这是一个聪明的安腾扩展。但像 Power(TM) 这样的芯片将会重新排序。您真正需要订购的是 记忆栅栏, , 也叫 记忆障碍. 。内存栅栏可以防止跨栅栏的内存操作重新排序,或者在某些情况下,可以防止在一个方向上重新排序。易失性与内存栅栏无关。

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

  • POSIX 线程
  • Windows(TM) 线程
  • 开放MP
  • 待定

基于 文章作者:Arch Robison(英特尔)

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

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

不。

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

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

仅当一个线程写入内容和另一个线程读取内容之间完全不需要延迟时,易失性才有用。但是,如果没有某种锁,您将不知道 什么时候 另一个线程写入数据,只是它是最新的可能值。

对于简单值(各种大小的 int 和 float),如果不需要显式同步点,则互斥体可能会过度杀伤力。如果您不使用某种互斥锁或锁,则应该将变量声明为 volatile。如果您使用互斥锁,那么一切就都准备好了。

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

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

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

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

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

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

正如保罗·麦肯尼 (Paul Mckenney) 在一篇著名的文章中指出的那样 Linux内核文档:

假定编译器将使用不受read_once()和write_once()保护的内存引用来完成您想要的操作。没有它们,编译器将有权进行各种“创造性”转换,这些转换在编译器屏障部分中涵盖。

READ_ONCE() 和 WRITE_ONCE() 被定义为引用变量的易失性强制转换。因此:

int y;
int x = READ_ONCE(y);

相当于:

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

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

总之,您应该将多个线程共享的变量标记为易失性,或者使用易失性强制转换来访问它们。


正如保罗·麦肯尼也指出的那样:

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


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

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

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

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

如果同步基元是外部函数,并且相关符号在编译单元外部可见(全局名称、导出指针、可能修改它们的导出函数),则编译器会将它们(或任何其他外部函数调用)视为相对于所有外部可见对象的内存栅栏。

否则,你就只能靠自己了。易失性可能是使编译器生成正确、快速代码的最佳工具。不过,当您需要 volatile 时,它​​通常不会是可移植的,并且它实际为您做什么很大程度上取决于系统和编译器。

不。

第一的, volatile 没有必要。还有许多其他操作提供有保证的多线程语义,但不使用 volatile. 。其中包括原子操作、互斥体等等。

第二, volatile 还不够。C 标准不为声明的变量的多线程行为提供任何保证 volatile.

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

一个例外是特定平台(例如 Visual Studio),它确实记录了多线程语义。

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top