编辑添加:感谢您提供的好答案。但是,如果您没有使用锁;例如,如果您正在使用barriers,或者使用原始的compare-and-swap代码直接和原子地修改共享变量...
没错,但即使您使用了volatile,CPU也可以将共享数据缓存到写入发布缓冲区中的任意时间长度。可能会影响您的优化集合并不完全与“volatile”禁用的优化集合相同。因此,如果您使用“volatile”,您是在依靠盲目的运气。
另一方面,如果您使用具有定义的多线程语义的同步原语,您可以保证事情会正常工作。此外,您不会承受“volatile”的巨大性能损失。那么为什么不这样做呢?
volatile
的解释是保留给可能由硬件引起变化的内存,而不是由软件引起的内存更改。对于GCC,应该在软件中使用Memory Barrier。这是代码:asm volatile("": : :"memory")
。微软的解释是内存可以由硬件和软件引起变化。 - jwwvolatile
不会强制数据可预测地写入实际内存,并且标准也没有要求这样做。在大多数现代计算机上也不会这样做,标准也没有要求这样做。 - David Schwartz有一种普遍的观念认为,关键字volatile对于多线程编程是有好处的。
Hans Boehm 指出,volatile只有三种可移植的用途:
volatile int Ready;
int Message[100];
void foo( int i ) {
Message[i/10] = 42;
Ready = 1;
}
您可能认为解决方案是标记所有内存引用为 volatile。那真是太愚蠢了。正如之前的引用所说,这只会减慢您的代码。更糟糕的是,它可能无法解决问题。即使编译器不重新排序引用,硬件也可能会这样做。在这个例子中,x86 硬件不会重新排序。像 Itanium(TM) 处理器这样的芯片也不会重新排序,因为 Itanium 编译器会为 volatile 存储插入内存屏障。这是一个聪明的 Itanium 扩展。但是像 Power(TM) 这样的芯片会重新排序。您真正需要的是 内存屏障,也称为 内存栅栏。内存屏障可以防止跨越栅栏的内存操作重新排序,或者在某些情况下,防止在一个方向上重新排序。Volatile 与内存屏障无关。
那么多线程编程的解决方案是什么?使用实现原子性和屏障语义的库或语言扩展。当按预期使用时,库中的操作将插入正确的屏障。以下是一些示例:
NO.
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 内存同步"说:
因此,如果您的变量在The following functions synchronize memory with respect to other threads:
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
来提供同步。int y;
int x = READ_ONCE(y);
等同于:
int y;
int x = *(volatile int *)&y;
因此,除非你进行 'volatile' 访问,否则无论你使用何种同步机制,都不能确保访问会发生仅一次。调用外部函数(例如 pthread_mutex_lock)可能会强制编译器对全局变量进行内存访问。但这仅在编译器无法确定外部函数是否更改这些全局变量时才会发生。采用复杂的过程间分析和链接时优化的现代编译器使得这种技巧变得毫无用处。
总之,你应该将多个线程共享的变量标记为 volatile 或者使用 volatile 强制类型转换进行访问。
但是看看 C11/C++11 会发生什么。
volatile 意味着我们必须去内存中获取或设置这个值。如果你不设置 volatile,编译后的代码可能会将数据存储在寄存器中很长一段时间。
这意味着你应该将在线程之间共享的变量标记为 volatile,以便你不会出现这样的情况:一个线程开始修改值,但在第二个线程来读取值之前没有写入其结果。
volatile 是一个编译器提示,它禁用了某些优化。编译器的输出汇编可能没有使用它也是安全的,但对于共享值,你应该始终使用它。
如果你没有使用系统提供的昂贵的线程同步对象,这一点尤其重要 - 例如,你可能有一个数据结构,可以通过一系列原子更改使其保持有效。许多不分配内存的堆栈就是这种数据结构的例子,因为你可以向堆栈添加一个值,然后移动末尾指针或者在移动末尾指针后从堆栈中删除一个值。当实现这样的结构时,volatile 变得至关重要,以确保你的原子指令实际上是原子的。
如果您需要在一个线程写入一些东西和另一个线程读取它之间绝对没有延迟,那么Volatile将非常有用。但是,如果没有某种形式的锁定,您就不知道其他线程写入数据的时间,只知道它是最近可能的值。
对于简单值(int和各种大小的float),如果您不需要显式的同步点,则互斥可能过度。如果您不使用互斥或某种类型的锁定,则应声明变量为volatile。如果您使用互斥,那么可以了。
对于复杂类型,必须使用互斥。对它们的操作是非原子的,因此,在没有互斥的情况下,您可能会读取到部分更改的版本。