为什么线程同步不需要易失关键字
Why is volatile keyword not needed for thread synchronisation?
我读到volatile
关键字不适合线程同步,实际上根本不需要它用于这些目的。
虽然我知道使用这个关键字是不够的,但我不明白为什么它完全没有必要。
例如,假设我们有两个线程,线程 A 只从共享变量读取,线程 B 只写入共享变量。通过例如 pthreads 互斥锁强制执行正确的同步。
IIUC,如果没有 volatile 关键字,编译器可能会查看线程 A 的代码并说:"变量在这里似乎没有被修改,但我们有很多读取;让我们只读取一次,缓存值并优化所有后续读取。它也可能会查看线程 B 的代码并说:"我们在这里对这个变量有很多写入,但没有读取;因此,不需要写入值,因此让我们优化所有写入。
这两种优化都是不正确的。
volatile
不足以同步线程,但对于线程之间共享的任何变量仍然是必需的。(注意:我现在读到,实际上volatile
不需要防止写入省略;所以我不知道如何防止这种不正确的优化(我知道我在这里错了。但是为什么?
例如,假设我们有两个线程,线程 A 只从共享变量读取,线程 B 只写入共享变量。通过例如 pthreads 互斥锁强制执行正确的同步。
IIUC,如果没有 volatile 关键字,编译器可能会查看线程 A 的代码并说:"变量在这里似乎没有被修改,但我们有很多读取;让我们只读取一次,缓存值并优化所有后续读取。它也可能会查看线程 B 的代码并说:"我们在这里对这个变量有很多写入,但没有读取;因此,不需要写入值,因此让我们优化所有写入。
与大多数线程同步原语一样,pthreads 互斥体操作显式定义了内存可见性语义。
平台要么支持pthreads,要么不支持。如果它支持 pthreads,则支持 pthreads 互斥锁。这些优化要么安全,要么不安全。如果他们是安全的,就没有问题。如果它们不安全,那么任何制造它们的平台都不支持 pthreads 互斥锁。
例如,你说"变量似乎没有在这里被修改",但它确实被修改了——另一个线程可以在那里修改它。除非编译器能够证明其优化不能破坏任何符合标准的程序,否则它无法做到这一点。符合标准的程序可以在另一个线程中修改变量。编译器要么支持 POSIX 线程,要么不支持。
碰巧的是,大多数情况下,大多数情况都会自动发生。编译器只是不知道互斥操作在内部做什么。另一个线程可以做的任何事情,互斥体操作本身都可以做。因此,编译器必须在进入和退出这些函数之前"同步"内存。例如,它不能在调用pthread_mutex_lock
时将值保留在寄存器中,因为据它所知,pthread_mutex_lock
访问内存中的该值。或者,如果编译器对互斥函数有特殊了解,这将包括了解这些调用中其他线程可访问的缓存值的无效性。
一个需要volatile
的平台几乎无法使用。对于对象可能对另一个线程可见或从另一个线程可见的特定情况,您需要每个函数或类的版本。在许多情况下,您几乎只需要使所有内容都volatile
,而不是在寄存器中缓存值是无法实现性能的。
你可能已经听过很多次了,volatile
语言中指定的语义不能与线程有效混合。它不仅不够,而且还禁用了许多完全安全且几乎必不可少的优化。
缩短已经给出的答案,您不需要将volatile
与互斥体一起使用,原因很简单:
- 如果编译器知道互斥操作是什么(通过识别pthread_*函数或因为你使用了
std::mutex
(,它就知道如何处理有关优化的访问(这甚至是std::mutex
所必需的(。 - 如果编译器无法识别它们,则pthread_*函数对其完全不透明,并且涉及任何类型的非本地持续时间对象的优化都不能跨不透明函数
使答案更短,不使用互斥锁或信号量,是一个错误。 一旦线程 B 释放互斥锁(并且线程 A 得到它(,寄存器中包含线程 B 的共享变量值的任何值都保证被写入缓存或内存,这将防止线程 A 运行时出现争用条件并读取此变量。
保证这一点的实现取决于体系结构/编译器。
关键字 volatile
告诉编译器将变量的任何写入或读取视为"可观察的副作用"。这就是它所做的一切。当然,可观察到的副作用不能被优化掉,并且必须按照程序指示的顺序出现在外界面前;编译器可能不会对彼此可观察到的副作用重新排序。但是,编译器可以自由地针对不可观察量对它们进行重新排序。因此,volatile
仅适用于访问内存映射硬件、Unix 样式的信号处理程序等。 对于线程间并发,请使用std::atomic
或更高级别的同步对象,如 mutex
、condition_variable
和 promise/future
。
- 易失性sig_atomic_t的内存安全性
- C++易失性:保证 32 位访问?
- 避免易失性和非易失性成员函数的代码重复
- 当 2 个线程共享同一物理内核时,具有错误共享的易失性增量在发布中的运行速度比在调试中慢
- 如何访问常量易失性 std::array?
- 为什么在 C++20 中弃用易失性?
- 根据 MSVC,具有易失性成员的结构不再是 POD
- 是否允许编译器优化掉局部易失性变量
- 访问共享内存而不使用易失性、std::atomic、信号量、互斥锁和自旋锁
- 如何避免对无锁程序使用易失性?
- C++:易失性实例中的易失性成员函数 - 将数组分配给指针是无效的转换?
- g++ 6.3,avx 内联函数上的 Kahan 求和用易失性关键字进行序列化
- 是什么让这种易失性打破了结构的指针算法?
- 如果不需要易失性,为什么 std::atomic 方法会提供易失性重载
- *(易失性无符号整数 *) 的含义 0x00 = 0x00;
- 我可以使用互斥锁或关键字(静态)代替C++中的易失性吗?
- 易失性关键字和 RAII 习语 (C++)
- 易失性关键字在C++中成员函数声明中的位置
- 为什么线程同步不需要易失关键字
- C/C++ 更罕见的关键字 - 寄存器、易失性、外部、显式