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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/2484980/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-27 23:36:59  来源:igfitidea点击:

Why is volatile not considered useful in multithreaded C or C++ programming?

c++cmultithreadingvolatilec++-faq

提问by Michael Ekstrand

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

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

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.

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

So, if you have a global int foo, and foois 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 fooshould be declared volatile(or, for multithreaded situations, accessed with memory-fenced load, which is probably a better a solution).

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

How and where am I wrong?

我错在哪里?

回答by jalf

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

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

However, the primitives we'd have to use for the remainingproperties also provide the ones that volatiledoes, so it is effectively unnecessary.

然而,我们必须用于其余属性的原语也提供了那些volatile,因此实际上是不必要的。

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

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

  • 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 volatilevariable 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 looksfine. But what if the instructions are reordered so the flag is set first?
  • 读/写实际上发生了(编译器不会只是将值存储在寄存器中并将更新主内存推迟到很晚之后)
  • 不会发生重新排序。假设我们使用一个volatile变量作为标志来指示是否准备好读取某些数据。在我们的代码中,我们只是在准备数据后设置标志,所以一切看起来都很好。但是如果指令被重新排序以便首先设置标志怎么办?

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

volatile确实保证第一点。它还保证在不同的易失性读/写之间不会发生重新排序。所有volatile内存访问都将按照指定的顺序进行。这就是我们所需要的全部内容volatile:操作 I/O 寄存器或内存映射硬件,但它对我们在多线程代码中没有帮助,因为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.

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

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

然而,内存屏障确保在达到屏障时执行所有挂起的读/写,因此它有效地为我们提供了我们自己需要的一切,从而变得volatile不必要。我们可以完全删除volatile限定符。

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

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

回答by Michael Ekstrand

You might also consider this from the Linux Kernel Documentation.

您也可以从Linux 内核文档中考虑这一点。

C programmers have often taken volatile to mean that the variable could be changed outside of the current thread of execution; as a result, they are sometimes tempted to use it in kernel code when shared data structures are being used. In other words, they have been known to treat volatile types as a sort of easy atomic variable, which they are not. The use of volatile in kernel code is almost never correct; this document describes why.

The key point to understand with regard to volatile is that its purpose is to suppress optimization, which is almost never what one really wants to do. In the kernel, one must protect shared data structures against unwanted concurrent access, which is very much a different task. The process of protecting against unwanted concurrency will also avoid almost all optimization-related problems in a more efficient way.

Like volatile, the kernel primitives which make concurrent access to data safe (spinlocks, mutexes, memory barriers, etc.) are designed to prevent unwanted optimization. If they are being used properly, there will be no need to use volatile as well. If volatile is still necessary, there is almost certainly a bug in the code somewhere. In properly-written kernel code, volatile can only serve to slow things down.

Consider a typical block of kernel code:

spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);

If all the code follows the locking rules, the value of shared_data cannot change unexpectedly while the_lock is held. Any other code which might want to play with that data will be waiting on the lock. The spinlock primitives act as memory barriers - they are explicitly written to do so - meaning that data accesses will not be optimized across them. So the compiler might think it knows what will be in shared_data, but the spin_lock() call, since it acts as a memory barrier, will force it to forget anything it knows. There will be no optimization problems with accesses to that data.

If shared_data were declared volatile, the locking would still be necessary. But the compiler would also be prevented from optimizing access to shared_data withinthe critical section, when we know that nobody else can be working with it. While the lock is held, shared_data is not volatile. When dealing with shared data, proper locking makes volatile unnecessary - and potentially harmful.

The volatile storage class was originally meant for memory-mapped I/O registers. Within the kernel, register accesses, too, should be protected by locks, but one also does not want the compiler "optimizing" register accesses within a critical section. But, within the kernel, I/O memory accesses are always done through accessor functions; accessing I/O memory directly through pointers is frowned upon and does not work on all architectures. Those accessors are written to prevent unwanted optimization, so, once again, volatile is unnecessary.

Another situation where one might be tempted to use volatile is when the processor is busy-waiting on the value of a variable. The right way to perform a busy wait is:

while (my_variable != what_i_want)
    cpu_relax();

The cpu_relax() call can lower CPU power consumption or yield to a hyperthreaded twin processor; it also happens to serve as a memory barrier, so, once again, volatile is unnecessary. Of course, busy-waiting is generally an anti-social act to begin with.

There are still a few rare situations where volatile makes sense in the kernel:

  • The above-mentioned accessor functions might use volatile on architectures where direct I/O memory access does work. Essentially, each accessor call becomes a little critical section on its own and ensures that the access happens as expected by the programmer.

  • Inline assembly code which changes memory, but which has no other visible side effects, risks being deleted by GCC. Adding the volatile keyword to asm statements will prevent this removal.

  • The jiffies variable is special in that it can have a different value every time it is referenced, but it can be read without any special locking. So jiffies can be volatile, but the addition of other variables of this type is strongly frowned upon. Jiffies is considered to be a "stupid legacy" issue (Linus's words) in this regard; fixing it would be more trouble than it is worth.

  • Pointers to data structures in coherent memory which might be modified by I/O devices can, sometimes, legitimately be volatile. A ring buffer used by a network adapter, where that adapter changes pointers to indicate which descriptors have been processed, is an example of this type of situation.

For most code, none of the above justifications for volatile apply. As a result, the use of volatile is likely to be seen as a bug and will bring additional scrutiny to the code. Developers who are tempted to use volatile should take a step back and think about what they are truly trying to accomplish.

C 程序员通常认为 volatile 意味着可以在当前执行线程之外更改变量;因此,当使用共享数据结构时,他们有时会想在内核代码中使用它。换句话说,众所周知,他们将 volatile 类型视为一种简单的原子变量,而事实并非如此。在内核代码中使用 volatile 几乎是不正确的;本文档说明了原因。

关于 volatile 需要理解的关键点是它的目的是抑制优化,这几乎从来不是人们真正想要做的。在内核中,必须保护共享数据结构免受不必要的并发访问,这是一项非常不同的任务。防止不需要的并发的过程还将以更有效的方式避免几乎所有与优化相关的问题。

与 volatile 一样,使并发访问数据安全(自旋锁、互斥锁、内存屏障等)的内核原语旨在防止不必要的优化。如果它们使用得当,也不需要使用 volatile。如果仍然需要 volatile,则几乎可以肯定代码中某处存在错误。在正确编写的内核代码中, volatile 只能起到减慢速度的作用。

考虑一个典型的内核代码块:

spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);

如果所有代码都遵循锁定规则,则在持有 the_lock 期间,shared_data 的值不会发生意外变化。可能想要使用该数据的任何其他代码都将等待锁定。自旋锁原语充当内存屏障 - 它们被明确写入这样做 - 这意味着不会在它们之间优化数据访问。所以编译器可能认为它知道 shared_data 中的内容,但是 spin_lock() 调用,因为它充当内存屏障,将迫使它忘记它知道的任何东西。访问该数据不会有优化问题。

如果 shared_data 被声明为 volatile,锁定仍然是必要的。但是,编译器也将被从优化访问shared_data防止的关键部分,当我们知道没有其他人可以用它来工作。持有锁时,shared_data 不是易失性的。在处理共享数据时,适当的锁定会使 volatile 变得不必要——而且可能有害。

易失性存储类最初用于内存映射 I/O 寄存器。在内核中,寄存器访问也应该受锁保护,但也不希望编译器“优化”临界区中的寄存器访问。但是,在内核中,I/O 内存访问总是通过访问器函数完成;直接通过指针访问 I/O 内存是不受欢迎的,并且不适用于所有架构。编写这些访问器是为了防止不必要的优化,因此,再次强调, volatile 是不必要的。

另一种可能倾向于使用 volatile 的情况是处理器忙于等待变量的值。执行忙等待的正确方法是:

while (my_variable != what_i_want)
    cpu_relax();

cpu_relax() 调用可以降低 CPU 功耗或让步于超线程双处理器;它也恰好用作内存屏障,因此,再一次, volatile 是不必要的。当然,忙等待通常是一种反社会行为。

仍然有一些罕见的情况在内核中 volatile 有意义:

  • 上述访问器函数可能在直接 I/O 内存访问有效的架构上使用 volatile。从本质上讲,每个访问器调用本身就变成了一个小关键部分,并确保访问按程序员的预期进行。

  • 改变内存但没有其他可见副作用的内联汇编代码有被 GCC 删除的风险。将 volatile 关键字添加到 asm 语句将阻止此删除。

  • jiffies 变量的特殊之处在于它每次被引用时可以具有不同的值,但无需任何特殊锁定即可读取它。所以 jiffies 可能是不稳定的,但强烈反对添加这种类型的其他变量。在这方面,Jiffies 被认为是一个“愚蠢的遗产”问题(Linus 的话);修复它会比它的价值更麻烦。

  • 指向相干存储器中可能被 I/O 设备修改的数据结构的指针有时可以合法地不稳定。网络适​​配器使用的环形缓冲区,其中适配器更改指针以指示哪些描述符已被处理,是此类情况的一个示例。

对于大多数代码,上述 volatile 的理由都不适用。因此,使用 volatile 很可能被视为一个错误,并将对代码进行额外的。想要使用 volatile 的开发人员应该退后一步,想想他们真正想要实现的目标是什么。

回答by Jeremy Friesner

I don't think you're wrong -- volatile is necessary to guarantee that thread A will see the value change, if the value is changed by something other than thread A. As I understand it, volatile is basically a way to tell the compiler "don't cache this variable in a register, instead be sure to always read/write it from RAM memory on every access".

我不认为你错了——如果值被线程 A 以外的其他东西改变了,必须保证线程 A 会看到值的变化。编译器“不要将此变量缓存在寄存器中,而是确保每次访问时始终从 RAM 内存读取/写入它”。

The confusion is because volatile isn't sufficient for implementing a number of things. In particular, modern systems use multiple levels of caching, modern multi-core CPUs do some fancy optimizations at run-time, and modern compilers do some fancy optimizations at compile time, and these all can result in various side effects showing up in a different order from the order you would expect if you just looked at the source code.

混淆是因为 volatile 不足以实现许多事情。特别是现代系统使用多级缓存,现代多核 CPU 在运行时做一些花哨的优化,现代编译器在编译时做一些花哨的优化,这些都会导致各种副作用以不同的方式出现。如果您只是查看源代码,则按照您期望的顺序排序。

So volatile is fine, as long as you keep in mind that the 'observed' changes in the volatile variable may not occur at the exact time you think they will. Specifically, don't try to use volatile variables as a way to synchronize or order operations across threads, because it won't work reliably.

所以 volatile 很好,只要你记住 volatile 变量中“观察到的”变化可能不会在你认为的确切时间发生。具体来说,不要尝试使用 volatile 变量作为跨线程同步或排序操作的方式,因为它无法可靠地工作。

Personally, my main (only?) use for the volatile flag is as a "pleaseGoAwayNow" boolean. If I have a worker thread that loops continuously, I'll have it check the volatile boolean on each iteration of the loop, and exit if the boolean is ever true. The main thread can then safely clean up the worker thread by setting the boolean to true, and then calling pthread_join() to wait until the worker thread is gone.

就个人而言,我对 volatile 标志的主要(仅?)用途是作为“pleaseGoAwayNow”布尔值。如果我有一个连续循环的工作线程,我会让它在循环的每次迭代中检查 volatile 布尔值,如果布尔值为真,则退出。然后,主线程可以通过将布尔值设置为 true 来安全地清理工作线程,然后调用 pthread_join() 等待工作线程消失。

回答by Potatoswatter

volatileis useful (albeit insufficient) for implementing the basic construct of a spinlock mutex, but once you have that (or something superior), you don't need another volatile.

volatile对于实现自旋锁互斥锁的基本构造很有用(尽管还不够),但是一旦你有了它(或更好的东西),你就不需要另一个volatile.

The typical way of multithreaded programming is not to protect every shared variable at the machine level, but rather to introduce guard variables which guide program flow. Instead of volatile bool my_shared_flag;you should have

多线程编程的典型方式不是在机器级别保护每个共享变量,而是引入引导程序流程的保护变量。而不是volatile bool my_shared_flag;你应该有

pthread_mutex_t flag_guard_mutex; // contains something volatile
bool my_shared_flag;

Not only does this encapsulate the "hard part," it's fundamentally necessary: C does not include atomic operationsnecessary to implement a mutex; it only has volatileto make extra guarantees about ordinaryoperations.

这不仅封装了“困难的部分”,而且从根本上是必要的:C 不包括实现互斥锁所需的原子操作;它只volatile需要对普通操作做出额外的保证。

Now you have something like this:

现在你有这样的事情:

pthread_mutex_lock( &flag_guard_mutex );
my_local_state = my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

pthread_mutex_lock( &flag_guard_mutex ); // may alter my_shared_flag
my_shared_flag = ! my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

my_shared_flagdoes not need to be volatile, despite being uncacheable, because

my_shared_flag尽管不可缓存,但不需要是易失性的,因为

  1. Another thread has access to it.
  2. Meaning a reference to it must have been taken sometime (with the &operator).
    • (Or a reference was taken to a containing structure)
  3. pthread_mutex_lockis a library function.
  4. Meaning the compiler can't tell if pthread_mutex_locksomehow acquires that reference.
  5. Meaning the compiler must assumethat pthread_mutex_lockmodifes the shared flag!
  6. So the variable must be reloaded from memory. volatile, while meaningful in this context, is extraneous.
  1. 另一个线程可以访问它。
  2. 这意味着必须在某个时候(与&操作员一起)对它进行引用。
    • (或者引用了一个包含结构)
  3. pthread_mutex_lock是一个库函数。
  4. 这意味着编译器无法判断是否pthread_mutex_lock以某种方式获取了该引用。
  5. 这意味着编译器必须假定pthread_mutex_lockmodifes共享的标志
  6. 所以变量必须从内存中重新加载。volatile,虽然在这种情况下有意义,但却是无关紧要的。

回答by zebrabox

For your data to be consistent in a concurrent environment you need two conditions to apply:

为了让您的数据在并发环境中保持一致,您需要应用两个条件:

1) Atomicity i.e if I read or write some data to memory then that data gets read/written in one pass and cannot be interrupted or contended due to e.g a context switch

1) 原子性,即,如果我读取或写入一些数据到内存,那么该数据将一次性读取/写入,并且不会因例如上下文切换而被中断或争用

2) Consistency i.e the order of read/write ops must be seento be the same between multiple concurrent environments - be that threads, machines etc

2)稠度,即读/写OPS的顺序必须被看作是多个并发环境之间是相同的-是线程,机器等

volatile fits neither of the above - or more particularly, the c or c++ standard as to how volatile should behave includes neither of the above.

volatile 不适合上述任何一个 - 或者更具体地说,关于 volatile 应该如何表现的 c 或 c++ 标准不包括上述任何一个。

It's even worse in practice as some compilers ( such as the intel Itanium compiler ) do attempt to implement some element of concurrent access safe behaviour ( i.e by ensuring memory fences ) however there is no consistency across compiler implementations and moreover the standard does not require this of the implementation in the first place.

在实践中情况更糟,因为一些编译器(例如英特尔安腾编译器)确实尝试实现并发访问安全行为的某些元素(即通过确保内存栅栏),但是编译器实现之间没有一致性,而且标准不要求这样做首先是实施。

Marking a variable as volatile will just mean that you are forcing the value to be flushed to and from memory each time which in many cases just slows down your code as you've basically blown your cache performance.

将变量标记为 volatile 仅意味着您每次都强制将值刷新到内存和从内存中刷新,这在许多情况下只会减慢代码速度,因为您基本上已经破坏了缓存性能。

c# and java AFAIK do redress this by making volatile adhere to 1) and 2) however the same cannot be said for c/c++ compilers so basically do with it as you see fit.

c# 和 java AFAIK 通过使 volatile 遵守 1) 和 2) 来解决这个问题,但是对于 c/c++ 编译器不能说同样的话,所以基本上按照你认为合适的方式使用它。

For some more in depth ( though not unbiased ) discussion on the subject read this

有关该主题的更深入(虽然不是无偏见的)讨论,请阅读此内容

回答by jpalecek

Your understanding really is wrong.

你的理解确实是错误的。

The property, that the volatile variables have, is "reads from and writes to this variable are part of perceivable behaviour of the program". That means this program works (given appropriate hardware):

volatile 变量具有的属性是“读取和写入该变量是程序可感知行为的一部分”。这意味着该程序有效(给定适当的硬件):

int volatile* reg=IO_MAPPED_REGISTER_ADDRESS;
*reg=1; // turn the fuel on
*reg=2; // ignition
*reg=3; // release
int x=*reg; // fire missiles

The problem is, this is not the property we want from thread-safe anything.

问题是,这不是我们想要的线程安全属性。

For example, a thread-safe counter would be just (linux-kernel-like code, don't know the c++0x equivalent):

例如,线程安全计数器将只是(类似 linux 内核的代码,不知道 c++0x 等效项):

atomic_t counter;

...
atomic_inc(&counter);

This is atomic, without a memory barrier. You should add them if necessary. Adding volatile would probably not help, because it wouldn't relate the access to the nearby code (eg. to appending of an element to the list the counter is counting). Certainly, you don't need to see the counter incremented outside your program, and optimisations are still desirable, eg.

这是原子的,没有内存屏障。如有必要,您应该添加它们。添加 volatile 可能无济于事,因为它不会与对附近代码的访问相关联(例如,将元素附加到计数器正在计数的列表中)。当然,您不需要在程序外看到计数器增加,并且仍然需要优化,例如。

atomic_inc(&counter);
atomic_inc(&counter);

can still be optimised to

仍然可以优化为

atomically {
  counter+=2;
}

if the optimizer is smart enough (it doesn't change the semantics of the code).

如果优化器足够聪明(它不会改变代码的语义)。

回答by Tony Delroy

The comp.programming.threads FAQ has a classic explanationby Dave Butenhof:

comp.programming.threads FAQ 有Dave Butenhof的经典解释

Q56: Why don't I need to declare shared variables VOLATILE?

I'm concerned, however, about cases where both the compiler and the threads library fulfill their respective specifications. A conforming C compiler can globally allocate some shared (nonvolatile) variable to a register that gets saved and restored as the CPU gets passed from thread to thread. Each thread will have it's own private value for this shared variable, which is not what we want from a shared variable.

In some sense this is true, if the compiler knows enough about the respective scopes of the variable and the pthread_cond_wait (or pthread_mutex_lock) functions. In practice, most compilers will not try to keep register copies of global data across a call to an external function, because it's too hard to know whether the routine might somehow have access to the address of the data.

So yes, it's true that a compiler that conforms strictly (but very aggressively) to ANSI C might not work with multiple threads without volatile. But someone had better fix it. Because any SYSTEM (that is, pragmatically, a combination of kernel, libraries, and C compiler) that does not provide the POSIX memory coherency guarantees does not CONFORM to the POSIX standard. Period. The system CANNOT require you to use volatile on shared variables for correct behavior, because POSIX requires only that the POSIX synchronization functions are necessary.

So if your program breaks because you didn't use volatile, that's a BUG. It may not be a bug in C, or a bug in the threads library, or a bug in the kernel. But it's a SYSTEM bug, and one or more of those components will have to work to fix it.

You don't want to use volatile, because, on any system where it makes any difference, it will be vastly more expensive than a proper nonvolatile variable. (ANSI C requires "sequence points" for volatile variables at each expression, whereas POSIX requires them only at synchronization operations -- a compute-intensive threaded application will see substantially more memory activity using volatile, and, after all, it's the memory activity that really slows you down.)

/---[ Dave Butenhof ]-----------------------[ [email protected] ]---\
| Digital Equipment Corporation 110 Spit Brook Rd ZKO2-3/Q18 |
| 603.881.2218, FAX 603.881.0120 Nashua NH 03062-2698 |
-----------------[ Better Living Through Concurrency ]----------------/

Q56:为什么我不需要声明共享变量 VOLATILE?

但是,我担心编译器和线程库都满足各自规范的情况。符合标准的 C 编译器可以将一些共享(非易失性)变量全局分配给一个寄存器,当 CPU 从一个线程传递到另一个线程时,该寄存器被保存和恢复。每个线程都会有它自己的共享变量的私有值,这不是我们想要的共享变量。

从某种意义上说这是真的,如果编译器足够了解变量和 pthread_cond_wait(或 pthread_mutex_lock)函数的各自范围。实际上,大多数编译器不会尝试在调用外部函数时保留全局数据的寄存器副本,因为很难知道例程是否可以以某种方式访问​​数据的地址。

所以是的,确实,严格(但非常积极)符合 ANSI C 的编译器可能无法在没有 volatile 的情况下与多线程一起工作。但最好有人修理它。因为任何不提供 POSIX 内存一致性保证的 SYSTEM(即,实用地说,内核、库和 C 编译器的组合)都不符合 POSIX 标准。时期。系统不能要求您在共享变量上使用 volatile 以获得正确的行为,因为 POSIX 只要求 POSIX 同步函数是必要的。

所以如果你的程序因为你没有使用 volatile 而中断,那就是一个 BUG。它可能不是 C 中的错误,或者线程库中的错误,或者内核中的错误。但这是一个系统错误,其中一个或多个组件必须修复它。

您不想使用 volatile,因为在任何有区别的系统上,它都会比适当的非易失性变量昂贵得多。(ANSI C 要求每个表达式中 volatile 变量的“序列点”,而 POSIX 仅在同步操作中需要它们——计算密集型线程应用程序将使用 volatile 看到更多的内存活动,毕竟,这是内存活动真的会让你慢下来。)

/---[ Dave Butenhof ]------------------------------[ [email protected] ]---\
| 数字设备公司 110 Spit Brook Rd ZKO2-3/Q18 |
| 603.881.2218,传真 603.881.0120 纳舒厄 NH 03062-2698 |
-----------------[通过并发改善生活]----------------/

Mr Butenhof covers much of the same ground in this usenet post:

Butenhof 先生在这篇 usenet 帖子中涵盖了大部分相同的内容

The use of "volatile" is not sufficient to ensure proper memory visibility or synchronization between threads. The use of a mutex is sufficient, and, except by resorting to various non-portable machine code alternatives, (or more subtle implications of the POSIX memory rules that are much more difficult to apply generally, as explained in my previous post), a mutex is NECESSARY.

Therefore, as Bryan explained, the use of volatile accomplishes nothing but to prevent the compiler from making useful and desirable optimizations, providing no help whatsoever in making code "thread safe". You're welcome, of course, to declare anything you want as "volatile" -- it's a legal ANSI C storage attribute, after all. Just don't expect it to solve any thread synchronization problems for you.

使用“volatile”不足以确保适当的内存可见性或线程之间的同步。使用互斥锁就足够了,而且,除了诉诸各种不可移植的机器代码替代方案,(或者更难以普遍应用的 POSIX 内存规则的更微妙的含义,如我之前的文章所述),互斥是必要的。

因此,正如 Bryan 解释的那样,使用 volatile 只会阻止编译器进行有用的和理想的优化,对使代码“线程安全”没有任何帮助。当然,欢迎您将您想要的任何内容声明为“易失性”——毕竟这是一个合法的 ANSI C 存储属性。只是不要指望它为您解决任何线程同步问题。

All that's equally applicable to C++.

所有这些都同样适用于 C++。

回答by Zack Yezek

This is all that "volatile" is doing: "Hey compiler, this variable could change AT ANY MOMENT (on any clock tick) even if there are NO LOCAL INSTRUCTIONS acting on it. Do NOT cache this value in a register."

这就是“易失性”所做的一切:“嘿编译器,即使没有本地指令作用于此,该变量也可能在任何时刻(在任何时钟滴答上)发生变化。不要将此值缓存在寄存器中。”

That is IT. It tells the compiler that your value is, well, volatile- this value may be altered at any moment by external logic (another thread, another process, the Kernel, etc.). It exists more or less solely to suppress compiler optimizations that will silently cache a value in a register that it is inherently unsafe to EVER cache.

这就对了。它告诉编译器您的值是易变的——这个值可能随时被外部逻辑(另一个线程、另一个进程、内核等)改变。它的存在或多或少仅是为了抑制编译器优化,这些优化将在寄存器中静默缓存一个值,它对 EVER 缓存本质上是不安全的。

You may encounter articles like "Dr. Dobbs" that pitch volatile as some panacea for multi-threaded programming. His approach isn't totally devoid of merit, but it has the fundamental flaw of making an object's users responsible for its thread-safety, which tends to have the same issues as other violations of encapsulation.

您可能会遇到诸如“Dr. Dobbs”之类的文章,这些文章将 volatile 视为多线程编程的灵丹妙药。他的方法并非完全没有优点,但它有一个基本缺陷,即让对象的用户对其线程安全负责,这往往与其他违反封装的问题存在相同的问题。

回答by david

According to my old C standard, “What constitutes an access to an object that has volatile- qualified type is implementation-defined”. So C compiler writers couldhave choosen to have "volatile" mean "thread safe access in a multi-process environment". But they didn't.

根据我的旧 C 标准,“什么构成对具有 volatile 限定类型的对象的访问是实现定义的”。因此,C 编译器编写者可以选择“易失性”意味着“多进程环境中的线程安全访问”。但他们没有。

Instead, the operations required to make a critical section thread safe in a multi-core multi-process shared memory environment were added as new implementation-defined features. And, freed from the requirement that "volatile" would provide atomic access and access ordering in a multi-process environment, the compiler writers prioritised code-reduction over historical implemention-dependant "volatile" semantics.

相反,在多核多进程共享内存环境中使临界区线程安全所需的操作被添加为新的实现定义功能。而且,摆脱了“易失性”将在多进程环境中提供原子访问和访问顺序的要求,编译器编写者将代码减少优先于依赖于历史实现的“易失性”语义。

This means that things like "volatile" semaphores around critical code sections, which do not work on new hardware with new compilers, might once have worked with old compilers on old hardware, and old examples are sometimes not wrong, just old.

这意味着像围绕关键代码部分的“易失性”信号量之类的东西,在使用新编译器的新硬件上不起作用,可能曾经在旧硬件上与旧编译器一起工作,而旧示例有时没有错,只是旧的。