以C编程语言和Pthreads作为线程库;线程之间共享的变量/结构是否需要声明为 volatile?假设它们可能受到锁的保护(也许是障碍)。
pthread POSIX 标准对此是否有任何发言权,这是依赖于编译器还是两者都不依赖?
编辑添加:感谢您的出色回答。但是如果你不使用锁呢?例如,如果您使用障碍怎么办?或者使用诸如比较和交换之类的原语直接和原子地修改共享变量的代码......
只要您使用锁来控制对变量的访问,就不需要对它进行 volatile 操作。事实上,如果你将 volatile 放在任何变量上,你可能已经错了。
答案是绝对的,毫不含糊的,不。除了正确的同步原语之外,您不需要使用“易失性”。需要做的一切都由这些原语完成。
'volatile' 的使用既不必要也不充分。这不是必需的,因为适当的同步原语就足够了。这还不够,因为它只会禁用一些优化,而不是所有可能会咬你的优化。例如,它不保证在另一个 CPU 上的原子性或可见性。
但是除非您使用 volatile,否则编译器可以自由地将共享数据缓存在寄存器中任意时间......如果您希望将数据写入可预测地写入实际内存,而不仅仅是缓存在寄存器中编译器自行决定,您需要将其标记为 volatile。或者,如果您只在离开修改它的函数后才访问共享数据,您可能没问题。但我建议不要依靠盲目的运气来确保将值从寄存器写回内存。
是的,但即使您确实使用了 volatile,CPU 也可以将共享数据缓存在写入发布缓冲区中任意时间长度。可以咬你的优化集与“volatile”禁用的优化集并不完全相同。因此,如果您使用“易失性”,则您是在依靠运气。
另一方面,如果您使用具有定义的多线程语义的同步原语,则可以保证一切正常。另外,您不会受到“易失性”的巨大性能影响。那么为什么不那样做呢?
我认为 volatile 的一个非常重要的属性是它使变量在修改时写入内存,并在每次访问时从内存中重新读取。这里的其他答案混合了易失性和同步性,并且从其他一些答案中可以清楚地看出,易失性不是同步原语(信用到期的信用)。
但是除非您使用 volatile,否则编译器可以自由地将共享数据缓存在寄存器中任意时间......如果您希望将数据写入可预测地写入实际内存,而不仅仅是缓存在寄存器中编译器自行决定,您需要将其标记为 volatile。或者,如果您只在离开修改它的函数后才访问共享数据,您可能没问题。但我建议不要依靠盲目的运气来确保将值从寄存器写回内存。
特别是在寄存器丰富的机器上(即,不是 x86),变量可以在寄存器中存在相当长的时间,一个好的编译器甚至可以在寄存器中缓存结构的一部分或整个结构。因此,您应该使用 volatile,但为了提高性能,还要将值复制到局部变量以进行计算,然后进行显式回写。从本质上讲,有效地使用 volatile 意味着在 C 代码中进行一些加载存储思考。
无论如何,您肯定必须使用某种操作系统级别提供的同步机制来创建正确的程序。
有关 volatile 弱点的示例,请参阅我在http://jakob.engbloms.se/archives/65上的 Decker 算法示例,这很好地证明了 volatile 无法同步。
有一个普遍的概念是关键字 volatile 有利于多线程编程。
Hans Boehm指出volatile 只有三种便携式用途:
如果您为了速度而使用多线程,那么减慢代码绝对不是您想要的。对于多线程编程,volatile 经常被错误地认为要解决两个关键问题:
我们先处理(1)。Volatile 不保证原子读取或写入。例如,129 位结构的易失性读取或写入在大多数现代硬件上不会是原子的。32 位 int 的 volatile 读取或写入在大多数现代硬件上是原子性的,但volatile 与它无关。如果没有 volatile,它可能是原子的。原子性是编译器的心血来潮。C 或 C++ 标准中没有任何内容说它必须是原子的。
现在考虑问题(2)。有时程序员将 volatile 视为关闭对 volatile 访问的优化。这在实践中基本上是正确的。但这只是易失性访问,而不是非易失性访问。考虑这个片段:
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) 等芯片将重新订购。订购真正需要的是内存栅栏,也称为内存屏障。内存栅栏防止跨栅栏的内存操作重新排序,或者在某些情况下,防止在一个方向上重新排序。易失性与内存栅栏无关。
那么多线程编程的解决方案是什么呢?使用实现原子和栅栏语义的库或语言扩展。当按预期使用时,库中的操作将插入正确的栅栏。一些例子:
根据我的经验,没有;当您写入这些值时,您只需要自己正确地互斥,或者构建您的程序,以便线程在需要访问依赖于另一个线程操作的数据之前停止。我的项目,x264,就是用这个方法;线程共享大量数据,但其中绝大多数不需要互斥锁,因为它要么是只读的,要么线程将等待数据变得可用并在需要访问它之前完成。
现在,如果您有许多线程在它们的操作中都被大量交错(它们在非常细粒度的级别上依赖于彼此的输出),这可能会更难——事实上,在这种情况下,我会考虑重新审视线程模型,看看是否可以通过线程之间的更多分离来更干净地完成它。
不。
Volatile
仅在读取可以独立于 CPU 读/写命令改变的内存位置时才需要。在线程的情况下,CPU 完全控制每个线程对内存的读/写,因此编译器可以假设内存是连贯的,并优化 CPU 指令以减少不必要的内存访问。
的主要用途volatile
是访问内存映射的 I/O。在这种情况下,底层设备可以独立于 CPU 更改内存位置的值。如果在这种情况下不使用volatile
,CPU 可能会使用以前缓存的内存值,而不是读取新更新的值。
POSIX 7 保证诸如pthread_lock
同步内存之类的功能
https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_11 “4.12 内存同步” 说:
以下函数相对于其他线程同步内存:
pthread_barrier_wait() pthread_cond_broadcast() pthread_cond_signal() pthread_cond_timedwait() pthread_cond_wait() pthread_create() pthread_join() pthread_mutex_lock() pthread_mutex_timedlock() pthread_mutex_trylock() pthread_mutex_unlock() pthread_spin_lock() pthread_spin_trylock() pthread_spin_unlock() pthread_rwlock_rdlock() pthread_rwlock_timedrdlock() pthread_rwlock_timedwrlock() pthread_rwlock_tryrdlock() pthread_rwlock_trywrlock() pthread_rwlock_unlock() pthread_rwlock_wrlock() sem_post() sem_timedwait() sem_trywait() sem_wait() semctl() semop() wait() waitpid()
因此,如果您的变量受到保护pthread_mutex_lock
,pthread_mutex_unlock
则它不需要进一步同步,因为您可能会尝试提供volatile
.
相关问题:
易失性意味着我们必须去内存来获取或设置这个值。如果不设置 volatile,编译后的代码可能会将数据长期保存在寄存器中。
这意味着您应该将线程之间共享的变量标记为易失性,这样您就不会出现一个线程开始修改值但在第二个线程出现并尝试读取值之前不写入结果的情况.
Volatile 是禁用某些优化的编译器提示。没有它,编译器的输出程序集可能是安全的,但您应该始终将它用于共享值。
如果您不使用系统提供的昂贵的线程同步对象,这一点尤其重要 - 例如,您可能有一个数据结构,您可以通过一系列原子更改使其保持有效。许多不分配内存的堆栈都是此类数据结构的示例,因为您可以向堆栈添加一个值,然后移动结束指针或在移动结束指针后从堆栈中删除一个值。在实现这样的结构时, volatile 对于确保您的原子指令实际上是原子的变得至关重要。
只有在一个线程写入内容和另一个线程读取内容之间绝对不需要延迟时,Volatile 才会有用。但是,如果没有某种锁,您将不知道其他线程何时写入数据,只知道它是最新的可能值。
对于简单的值(不同大小的 int 和 float),如果您不需要显式同步点,则互斥锁可能会过大。如果你不使用互斥锁或某种锁,你应该声明变量 volatile。如果您使用互斥锁,则一切就绪。
对于复杂的类型,您必须使用互斥锁。对它们的操作是非原子的,因此您可以在没有互斥锁的情况下阅读半修改版本。
根本原因是 C 语言语义基于单线程抽象机。只要程序在抽象机器上的“可观察行为”保持不变,编译器就有权转换程序。它可以合并相邻或重叠的内存访问,多次重做内存访问(例如在寄存器溢出时),或者简单地丢弃内存访问,如果它认为程序的行为在单个线程中执行时不会改变。因此,您可能会怀疑,如果程序实际上应该以多线程方式执行,那么行为确实会发生变化。
正如 Paul Mckenney 在著名的Linux 内核文档中指出的那样:
_must_not_ 假定编译器将使用不受 READ_ONCE() 和 WRITE_ONCE() 保护的内存引用执行您想要的操作。没有它们,编译器有权进行各种“创造性”转换,这些转换在编译器屏障部分中进行了介绍。
READ_ONCE() 和 WRITE_ONCE() 被定义为对引用变量的 volatile casts。因此:
int y;
int x = READ_ONCE(y);
相当于:
int y;
int x = *(volatile int *)&y;
因此,除非您进行“易失性”访问,否则无论您使用何种同步机制,都不能保证访问只会发生一次。调用外部函数(例如 pthread_mutex_lock)可能会强制编译器对全局变量进行内存访问。但这仅在编译器无法确定外部函数是否更改这些全局变量时才会发生。采用复杂的过程间分析和链接时间优化的现代编译器使这个技巧变得毫无用处。
总之,您应该将多个线程共享的变量标记为 volatile 或使用 volatile casts 访问它们。
正如保罗麦肯尼所指出的:
当他们讨论您不希望您的孩子知道的优化技术时,我看到了他们眼中的闪光!
但是看看C11/C++11会发生什么。
有些人显然假设编译器将同步调用视为内存屏障。“Casey”假设只有一个 CPU。
如果同步原语是外部函数并且所讨论的符号在编译单元之外可见(全局名称、导出的指针、可能修改它们的导出函数),那么编译器会将它们 - 或任何其他外部函数调用 - 视为关于所有外部可见对象的内存栅栏。
否则,你就靠自己了。volatile 可能是使编译器生成正确、快速代码的最佳工具。但是,它通常不会是可移植的,当您需要 volatile 时,它实际上为您做了什么,很大程度上取决于系统和编译器。
线程之间共享的变量应声明为“volatile”。这告诉编译器当一个线程写入这些变量时,写入应该是内存(而不是寄存器)。
不。
首先,volatile
没有必要。还有许多其他操作提供了不使用的有保证的多线程语义volatile
。这些包括原子操作、互斥体等。
二volatile
是不够。C 标准不对声明的变量的多线程行为提供任何保证volatile
。
因此,既不是必要的也不是充分的,使用它没有多大意义。
一个例外是特定平台(例如 Visual Studio),它确实记录了多线程语义。