为什么 volatile 在多线程 C 或 C++ 编程中没有用?

人气:342 发布:2022-10-16 标签: multithreading volatile c++ c c++-faq

问题描述

正如我最近发布的这个答案中所示,我似乎对 volatile 在多线程编程上下文中的实用性(或缺乏实用性)感到困惑.

As demonstrated in this answer I recently posted, I seem to be confused about the utility (or lack thereof) of volatile in multi-threaded programming contexts.

我的理解是:任何时候一个变量可能在一段代码访问它的控制流之外被改变,这个变量应该被声明为volatile.信号处理程序、I/O寄存器、被另一个线程修改的变量都构成了这种情况.

My understanding is this: any time a variable may be changed outside the flow of control of a piece of code accessing it, that variable should be declared to be volatile. Signal handlers, I/O registers, and variables modified by another thread all constitute such situations.

因此,如果您有一个全局 int foo,并且 foo 由一个线程读取并由另一个线程以原子方式设置(可能使用适当的机器指令),则读取线程看到这种情况的方式与看到由信号处理程序调整或由外部硬件条件修改的变量相同的方式,因此 foo 应该声明为 volatile (或者,对于多线程情况,通过内存隔离负载访问,这可能是更好的解决方案).

So, if you have a global int foo, and foo is read by one thread and set atomically by another thread (probably using an appropriate machine instruction), the reading thread sees this situation in the same way it sees a variable tweaked by a signal handler or modified by an external hardware condition and thus foo should be declared volatile (or, for multithreaded situations, accessed with memory-fenced load, which is probably a better a solution).

我哪里错了?

推荐答案

volatile 在多线程上下文中的问题是它不能提供 all 我们的保证需要.它确实有一些我们需要的属性,但不是全部,所以我们不能依赖 volatile 单独.

The problem with volatile in a multithreaded context is that it doesn't provide all the guarantees we need. It does have a few properties we need, but not all of them, so we can't rely on volatile alone.

然而,我们必须为 remaining 属性使用的原语也提供了 volatile 所做的那些,因此实际上没有必要.

However, the primitives we'd have to use for the remaining properties also provide the ones that volatile does, so it is effectively unnecessary.

对于共享数据的线程安全访问,我们需要保证:

For thread-safe accesses to shared data, we need a guarantee that:

实际上发生了读/写(编译器不会只是将值存储在寄存器中,而是将更新主内存推迟到很久以后)不会发生重新排序.假设我们使用 volatile 变量作为标志来指示某些数据是否已准备好被读取.在我们的代码中,我们只是在准备好数据后设置了标志,所以所有看起来都很好.但是如果指令被重新排序以设置标志first呢? the read/write actually happens (that the compiler won't just store the value in a register instead and defer updating main memory until much later) that no reordering takes place. Assume that we use a volatile variable as a flag to indicate whether or not some data is ready to be read. In our code, we simply set the flag after preparing the data, so all looks fine. But what if the instructions are reordered so the flag is set first?

volatile 确实保证了第一点.它还保证在不同的易失性读/写之间不会发生重新排序.所有 volatile 内存访问都将按照它们指定的顺序发生.这就是 volatile 的用途所需要的全部内容:操作 I/O 寄存器或内存映射硬件,但在 volatile 的多线程代码中它对我们没有帮助对象通常只用于同步访问非易失性数据.这些访问仍然可以相对于 volatile 重新排序.

volatile does guarantee the first point. It also guarantees that no reordering occurs between different volatile reads/writes. All volatile memory accesses will occur in the order in which they're specified. That is all we need for what volatile is intended for: manipulating I/O registers or memory-mapped hardware, but it doesn't help us in multithreaded code where the volatile object is often only used to synchronize access to non-volatile data. Those accesses can still be reordered relative to the volatile ones.

防止重新排序的解决方案是使用内存屏障,它向编译器和 CPU 都表明在这一点上不能对内存访问进行重新排序.在我们的 volatile 变量访问周围放置这样的障碍可确保即使是非 volatile 访问也不会在 volatile 中重新排序,从而允许我们编写线程安全的代码.

The solution to preventing reordering is to use a memory barrier, which indicates both to the compiler and the CPU that no memory access may be reordered across this point. Placing such barriers around our volatile variable access ensures that even non-volatile accesses won't be reordered across the volatile one, allowing us to write thread-safe code.

但是,内存屏障也确保在达到屏障时执行所有挂起的读/写操作,因此它有效地为我们提供了我们需要的一切,使得 volatile不必要.我们可以完全删除 volatile 限定符.

However, memory barriers also ensure that all pending reads/writes are executed when the barrier is reached, so it effectively gives us everything we need by itself, making volatile unnecessary. We can just remove the volatile qualifier entirely.

从 C++11 开始,原子变量 (std::atomic<T>) 为我们提供了所有相关保证.

Since C++11, atomic variables (std::atomic<T>) give us all of the relevant guarantees.

131