为什么volatile在multithreadingC或C ++编程中不被认为是有用的?

正如我最近发表的这个答案所展示的,我似乎对multithreading编程环境中volatile的效用(或缺乏)感到困惑。

我的理解是这样的:任何时候一个variables可能会被改变而不是访问它的一段代码的控制stream程,这个variables应该被声明为volatile 。 信号处理程序,I / O寄存器和由另一个线程修改的variables都是这种情况。

所以,如果你有一个全局的int foo ,并且foo被一个线程读取并且由另一个线程primefaces地设置(可能使用适当的机器指令),读线程就会看到这种情况,就像看到一个信号调整的variables一样处理程序或由外部硬件条件修改,因此foo应该被声明为volatile (或者,对于multithreading情况,使用内存隔离加载进行访问,这可能是更好的解决scheme)。

我的方法和错误在哪里?

multithreading环境中volatile的问题在于它没有提供我们需要的所有保证。 它确实有我们需要的一些属性,但不是全部,所以我们不能单靠volatile

然而,我们不得不使用剩余属性的原语也提供了volatile的原语,所以实际上是不必要的。

为了线程安全地访问共享数据,我们需要保证:

  • 实际上发生了读/写(编译器不会将值存储在寄存器中,而是延迟更新主存储器,直到更晚)
  • 没有重新sorting。 假设我们使用volatilevariables作为标志来指示某些数据是否准备好被读取。 在我们的代码中,我们只需在准备数据之后设置标志,所以看起来都很好。 但是如果指令是重新sorting的,那么标志是设置的呢?

volatile确实保证了第一点。 它还保证在不同的易失性读/写之间不发生重sorting。 所有volatile内存访问将按照指定的顺序进行。 这就是我们所需要的: volatile操作的目的是:操作I / O寄存器或内存映射硬件,但是它并不能帮助我们在multithreading代码中使用volatile对象来同步对非易失性数据的访问。 这些访问仍然可以相对于volatile的重新sorting。

防止重新sorting的解决scheme是使用一个内存屏障 ,它向编译器和CPU指出在这一点上没有内存访问可以重新sorting 。 围绕我们的易失性variables访问放置这样的障碍可以确保即使是非易失性访问也不会在易失性访问中重新sorting,从而允许我们编写线程安全的代码。

但是,内存障碍确保了所有待处理的读/写操作在达到屏障时执行,因此它实际上为我们提供了我们自己需要的所有东西,使得volatile变得不必要。 我们可以完全删除volatile限定符。

由于C ++ 11,primefacesvariables( std::atomic<T> )给了我们所有的相关保证。

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

C程序员经常使用volatile来表示variables可以在当前的执行线程之外被改变; 因此,当共享数据结构正在使用时,他们有时试图在内核代码中使用它。 换句话说,他们已经知道把易失性types看作是一种简单的primefacesvariables,而事实并非如此。 在内核代码中使用volatile几乎是不正确的。 这个文件描述了原因。

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

像volatile一样,可以并发访问数据安全的内核原语(自旋锁,互斥锁,内存屏障等)被devise用来防止不必要的优化。 如果使用得当,就没有必要使用易失性了。 如果volatile仍然是必要的,那么代码中几乎肯定会有一个bug。 在正确编写的内核代码中,volatile只能用来减慢速度。

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

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

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

如果shared_data被声明为volatile,那么locking仍然是必要的。 但是,当我们知道没有其他人可以使用它时,编译器也不能在关键部分优化对shared_data的访问。 在locking期间,shared_data不是易失性的。 在处理共享数据时,正确的locking会导致不必要的 – 且可能有害的。

易失性存储类最初是用于内存映射I / O寄存器的。 在内核中,寄存器访问也应该由锁保护,但是也不希望编译器在关键部分“优化”寄存器访问。 但是,在内核中,I / O内存访问总是通过访问函数完成的。 直接通过指针访问I / O存储器被忽视,并且在所有体系结构上都不起作用。 这些访问器是为防止不必要的优化而编写的,因此再次,volatile是不必要的。

另外一种情况是,当处理器忙着等待一个variables的值时,可能会使用volatile。 执行繁忙等待的正确方法是:

 while (my_variable != what_i_want) cpu_relax(); 

cpu_relax()调用可以降低CPU功耗,也可以使用超线程双处理器; 它也恰好作为记忆障碍,所以再一次,挥发性是不必要的。 当然,等待一般是一种反社会行为。

在内核中volatile还是有意义的:

  • 上述访问函数可能会在直接I / O内存访问工作的架构上使用volatile。 从本质上讲,每个访问者调用本身就成为一个关键部分,并且确保访问者按照程序员的预期发生。

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

  • jiffiesvariables的特殊之处在于它每次被引用时都可以有不同的值,但是它可以在没有任何特殊的locking的情况下被读取。 所以jiffies可以是不稳定的,但是这种types的其他variables的增加是强烈的皱起了眉头。 Jiffies在这方面被认为是“愚蠢的遗产”(Linus的话) 修复它会比它的价值更麻烦。

  • 指向可能由I / O设备修改的连贯内存中的数据结构有时可以合法地变化。 一个networking适配器使用的环形缓冲区,其中该适配器改变指示器来指示已经处理了哪个描述符,就是这种情况的一个例子。

对于大多数代码来说,上面没有任何适用于volatile的理由。 因此,volatile的使用很可能会被视为一个bug,并会对代码进行额外的审查。 想要使用volatile的开发人员应该退后一步,思考他们真正想要完成的事情。

我不认为你是错的 – 挥发性是必要的,以保证线程A会看到价值的变化,如果价值改变了线程以外的东西。据我所知,易失性基本上是一种方式来告诉编译器“不要将这个variablescaching在一个寄存器中,而是一定要在每次访问时总是从RAM内存读取/写入”。

混乱是因为挥发性不足以实施许多事情。 特别是,现代系统使用多级caching,现代多核CPU在运行时进行一些奇特的优化,而现代编译器在编译时做了一些奇妙的优化,这些都可能导致不同的副作用出现在不同的如果您只是查看源代码,则可以从您期望的顺序中下订单。

所以,波动性是好的,只要你记住,挥发性variables的“观察”变化可能不会发生在你认为的确切时间。 特别是,不要尝试使用volatilevariables作为跨线程同步或者sorting操作的一种方式,因为它不能可靠地工作。

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

你的理解确实是错误的。

易失variables所具有的属性是“对这个variables的读取和写入是程序的可感知行为的一部分”。 这意味着这个程序的工作(给定适当的硬件):

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

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

例如,一个线程安全的计数器就是(linux-kernel-like的代码,不知道c ++ 0x等价物):

 atomic_t counter; ... atomic_inc(&counter); 

这是primefaces的,没有记忆障碍。 如有必要,您应该添加它们。 添加volatile可能不会有帮助,因为它不会影响对附近代码的访问(例如将元素添加到计数器正在计数的列表中)。 当然,你不需要看到你的程序外增加的计数器,优化仍然是可取的,例如。

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

仍然可以优化到

 atomically { counter+=2; } 

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

volatile对于实现自旋锁互斥体的基本构造是有用的(虽然不足够),但是一旦你拥有了这个(或者更优越的),你就不需要另一个volatile

multithreading编程的典型方法不是保护机器级别的每个共享variables,而是引入引导程序stream程的保护variables。 而不是volatile bool my_shared_flag; 你应该有

 pthread_mutex_t flag_guard_mutex; // contains something volatile bool my_shared_flag; 

这不仅封装了“硬件部分”,它基本上是必需的:C不包括实现互斥锁所必需的primefaces操作 ; 只有对日常业务做出额外保证才有volatile

现在你有这样的东西:

 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_flag不需要变化,尽pipe是不可caching的,因为

  1. 另一个线程有权访问它。
  2. 这意味着必须在某个时候使用( &运算符)。
    • (或者引用一个包含结构)
  3. pthread_mutex_lock是一个库函数。
  4. 这意味着编译器无法确定pthread_mutex_lock是否获取了该引用。
  5. 意思是编译器必须假定 pthread_mutex_lock修改共享标志
  6. 所以variables必须从内存中重新加载。 volatile ,而在这方面有意义,是无关紧要的。

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

1)primefaces性,即如果我读或写一些数据到内存,那么数据读取/写入一次,不能被打断或争夺由于例如上下文切换

2)一致性,即读/写操作的顺序必须被看作是在多个并发环境中相同 – 是线程,机器等

volatile不符合上述两者 – 或者更具体地说,不适用于c或c ++标准,以至于挥发性应该如何performance不包括上述两者。

实际上,一些编译器(如英特尔安腾编译器)尝试实现某些并发访问安全行为元素(即通过确保内存隔离),在实践中甚至更糟糕,但编译器实现之间没有一致性,而且标准不要求首先是执行。

将variables标记为volatile将意味着您每次都强制将值从内存中刷新,这在很多情况下会降低代码的速度,因为您基本上已经吹嘘了caching的性能。

c#和java AFAIK通过使volatile符合1)和2)来解决这个问题,但是对于c / c ++编译器来说,不能这么说,所以基本上可以这么做。

对于这个问题的更深入的讨论(虽然不是没有偏见的)

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

Q56:为什么我不需要声明共享variablesVOLATILE?

然而,我担心的是编译器和线程库都满足各自规范的情况。 符合的C编译器可以全局地分配一些共享(非易失性)variables到一个寄存器,该寄存器在CPU从线程传递到线程时被保存和恢复。 每个线程都会为这个共享variables拥有自己的私有值,这不是我们想要的共享variables。

在某种意义上,这是真的,如果编译器足够了解variables和pthread_cond_wait(或pthread_mutex_lock)函数的相应范围。 在实践中,大多数编译器不会尝试通过调用外部函数来保存全局数据的注册副本,因为很难知道例程是否可以以某种方式访问​​数据的地址。

所以是的,一个严格遵守(但是非常积极的)ANSI C的编译器可能无法在multithreading中工作而没有volatile。 但有人最好修复它。 因为没有提供POSIX内存一致性保证的任何SYSTEM(即实用,内核,库和C编译器的组合)不符合POSIX标准。 期。 系统不要求您对共享variables使用volatile来实现正确的行为,因为POSIX只需要POSIX同步function。

所以如果你的程序因为没有使用volatile而中断,这是一个BUG。 它可能不是C中的一个bug,也可能不是线程库中的一个bug,也可能是内核中的一个bug。 但是这是一个系统错误,一个或多个这些组件将不得不解决它。

你不想使用volatile,因为在任何有区别的系统上,它比合适的非易失性variables要贵得多。 (ANSI C要求每个expression式的volatilevariables都有“顺序点”,而POSIX只需要在同步操作中使用它们 – 一个计算密集型的线程应用程序将使用volatile来看到更多的内存活动,毕竟这是内存活动真的让你失望。)

/ — [Dave Butenhof] ———————– [butenhof@zko.dec.com] — \
| 数字设备公司110 Spit Brook Rd ZKO2-3 / Q18 |
| 603.881.2218,传真603.881.0120 Nashua NH 03062-2698 |
—————– [通过并发更好地生活] —————- /

Butenhof先生在这个Usenetpost中涵盖了很多相同的内容 :

使用“volatile”不足以保证线程之间正确的内存可视性或同步。 互斥体的使用已经足够了,除了使用各种不可移植的机器代码替代方法(或者POSIX存储器规则的更微妙的含义,如我以前的文章中所解释的,通常更难以应用),互斥是必要的。

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

所有这一切同样适用于C ++。

根据我的旧C标准, “什么构成对具有volatile限定types的对象的访问是实现定义的” 。 所以C编译器作者可能select“volatile”意味着“在多进程环境中的线程安全访问” 。 但他们没有。

相反,在多核多进程共享内存环境中使关键节线程安全的操作被添加为新的实现定义的特性。 而且,摆脱了“易失性”在多进程环境中提供primefaces访问和访问顺序的要求,编译器编写者将历史实现相关的“易失性”语义优先于代码减less。

这意味着围绕关键代码段的“易失性”信号量(这些新的硬件无法在新的编译器上运行)可能曾经在旧的硬件上与老的编译器一起工作过,旧的例子有时候并没有错,只是老的。

这就是“易失性”所做的一切:“嘿编译器,即使没有本地指令作用,这个variables也可以在任何时刻改变(不pipe是在任何时钟周期),不要将这个值caching在寄存器中。

这就对了。 它告诉编译器你的值是volatile的 – 这个值可能随时被外部逻辑(另一个线程,另一个进程,内核等)改变。 它或多或less地仅仅是为了抑制编译器优化,它将静默地caching寄存器中的值,这对EVERcaching本质上是不安全的。

您可能会遇到诸如“Dobbs博士”这样的文章,将其作为multithreading编程的一些灵丹妙药。 他的方法并不完全没有优点,但它有一个使对象的用户对其线程安全负责的根本缺陷,这往往与其他违反封装的问题相同。