什么时候应该在C#中使用volatile关键字?

任何人都可以提供一个很好的解释在C#中的挥发性关键字? 它解决了哪些问题,哪些不解决? 在这种情况下,它会救我使用locking?

我不认为有比Eric Lippert更好的回答(原文的重点):

在C#中,“volatile”不仅意味着“确保编译器和抖动不会对此variables执行任何代码重新sorting或注册caching优化”。 这也意味着“告诉处理器做任何他们需要做的事情,以确保我正在读取最新值,即使这意味着停止其他处理器并使其与主存储器同步高速caching”。

事实上,最后一点是谎言。 易失性读写的真实语义比我在这里概述的复杂得多, 事实上, 他们并不确保每个处理器都停止正在进行的操作,并更新主内存的caching。 相反, 它们提供了关于在读取和写入之前和之后如何访问存储器的较弱保证,可以被观察到相对于彼此sorting 。 某些操作(例如创build新线程,inputlocking或使用Interlocked系列方法之一)会为观察sorting提供更强的保证。 如果您需要更多的细节,请阅读C#4.0规范的第3.10节和第10.5.3节。

坦率地说, 我劝阻你永远做一个动荡的领域 。 易变的字段表示你正在做一些疯狂的事情:你正试图在两个不同的线程上读取和写入相同的值,而没有locking位置。 锁保证在锁内部读取或修改的内存被观察为一致的,锁保证一次只有一个线程访问给定的内存块,依此类推。 锁太慢的情况很less,因为你不了解确切的内存模型,所以你会错误地得到代码的可能性非常大。 除了互锁操作最普通的用法之外,我不试图编写任何低locking代码。 我把“易变”的用法留给了真正的专家。

进一步阅读请参阅:

  • 了解低锁技术在multithreading应用程序中的影响
  • Sayonara易变

如果您想要获得关于volatile关键字的更多技术信息,请考虑以下程序(我正在使用DevStudio 2005):

 #include <iostream> void main() { int j = 0; for (int i = 0 ; i < 100 ; ++i) { j += i; } for (volatile int i = 0 ; i < 100 ; ++i) { j += i; } std::cout << j; } 

使用标准优化(释放)编译器设置,编译器创build以下汇编器(IA32):

 void main() { 00401000 push ecx int j = 0; 00401001 xor ecx,ecx for (int i = 0 ; i < 100 ; ++i) 00401003 xor eax,eax 00401005 mov edx,1 0040100A lea ebx,[ebx] { j += i; 00401010 add ecx,eax 00401012 add eax,edx 00401014 cmp eax,64h 00401017 jl main+10h (401010h) } for (volatile int i = 0 ; i < 100 ; ++i) 00401019 mov dword ptr [esp],0 00401020 mov eax,dword ptr [esp] 00401023 cmp eax,64h 00401026 jge main+3Eh (40103Eh) 00401028 jmp main+30h (401030h) 0040102A lea ebx,[ebx] { j += i; 00401030 add ecx,dword ptr [esp] 00401033 add dword ptr [esp],edx 00401036 mov eax,dword ptr [esp] 00401039 cmp eax,64h 0040103C jl main+30h (401030h) } std::cout << j; 0040103E push ecx 0040103F mov ecx,dword ptr [__imp_std::cout (40203Ch)] 00401045 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (402038h)] } 0040104B xor eax,eax 0040104D pop ecx 0040104E ret 

查看输出,编译器决定使用ecx寄存器来存储jvariables的值。 对于非易失性循环(第一个),编译器将i分配给eax寄存器。 非常坦率的。 虽然有一些有趣的地方 – lea ebx,[ebx]指令实际上是一个多字节nop指令,所以循环跳转到一个16字节alignment的存储器地址。 另一个是使用edx递增循环计数器,而不是使用inc eax指令。 与inc reg指令相比,add reg,reg指令在一些IA32内核上的延迟较低,但从不具有较高的延迟。

现在用volatile循环计数器进行循环。 计数器存储在[esp],volatile关键字告诉编译器应该始终从内存中读取/写入数据,并且永远不会分配给寄存器。 甚至在更新计数器值时,编译器甚至不会将加载/增量/存储作为三个不同的步骤(加载eax,inc eax,保存eax),而是直接在单个指令(add mem ,REG)。 代码的创build方式确保循环计数器的值始终在单个CPU内核的上下文中保持最新。 数据上的任何操作都不会导致损坏或数据丢失(因此不会使用load / inc / store,因为值会在inc中更改,从而在store中丢失)。 由于中断只能在当前指令完成后才能被处理,即使内存不alignment,数据也不会被破坏。

一旦将第二个CPU引入系统,volatile关键字将不会防止另一个CPU同时更新的数据。 在上面的例子中,您需要将数据进行非alignment以获得潜在的损坏。 如果数据不能自动处理,volatile关键字将不会防止潜在的破坏,例如,如果循环计数器的types是long long(64位),则需要两个32位操作来更新该值,在哪个中断可能发生并改变数据。

因此,volatile关键字只适用于小于或等于本地寄存器大小的alignment数据,因此操作总是primefaces性的。

volatile关键字被认为是用于IO操作,其中IO将不断地改变,但具有恒定的地址,例如存储器映射的UART设备,并且编译器不应该继续重复使用从地址读取的第一个值。

如果你正在处理大量数据或有多个CPU,那么你需要一个更高级别的(OS)locking系统来正确处理数据访问。

如果您正在使用.NET 1.1,则在执行双重检查locking时需要使用volatile关键字。 为什么? 因为在.NET 2.0之前,以下情况可能会导致第二个线程访问一个非null,但不完全构造的对象:

  1. 线程1询问一个variables是否为空。 //if(this.foo == null)
  2. 线程1确定variables为空,所以input一个锁。 //lock(this.bar)
  3. 线程1再次询问variables是否为空。 //if(this.foo == null)
  4. 线程1仍然确定该variables为空,所以它调用一个构造函数并将该值赋予该variables。 //this.foo = new Foo();

在.NET 2.0之前,在构造函数完成运行之前,可以为this.foo分配新的Foo实例。 在这种情况下,第二个线程可以进入(在线程1调用Foo的构造函数期间),并体验以下内容:

  1. 线程2询问variables是否为空。 //if(this.foo == null)
  2. 线程2确定variables不是空的,所以试图使用它。 //this.foo.MakeFoo()

在.NET 2.0之前,你可以声明this.foo为volatile来解决这个问题。 从.NET 2.0开始,您不再需要使用volatile关键字来完成双重检查的locking。

维基百科实际上有一个关于双重检查locking的好文章,并简要介绍了这个主题: http : //en.wikipedia.org/wiki/Double-checked_locking

来自MSDN :volatile修饰符通常用于multithreading访问的字段,而不使用lock语句来序列化访问。 使用volatile修饰符可确保一个线程检索另一个线程写入的最新值。

有时编译器会优化一个字段并使用一个寄存器来存储它。 如果线程1写入字段,另一个线程访问它,由于更新存储在一个寄存器(而不是内存),第二个线程会得到陈旧的数据。

你可以把volatile关键字想象成对编译器“我希望你把这个值存储在内存中”。 这保证了第二个线程获取最新的值。

CLR喜欢优化指令,所以当你访问代码中的字段时,它可能并不总是访问字段的当前值(可能来自堆栈等)。 将字段标记为volatile确保该字段的当前值由指令访问。 当程序中的并发线程或操作系统中运行的其他代码可以修改该值(在非lockingscheme中)时,这非常有用。

你显然失去了一些优化,但它确实使代码更简单。

编译器有时会改变代码中的语句顺序来优化它。 通常这在单线程环境中不是问题,但是在multithreading环境中可能是个问题。 看下面的例子:

  private static int _flag = 0; private static int _value = 0; var t1 = Task.Run(() => { _value = 10; /* compiler could switch these lines */ _flag = 5; }); var t2 = Task.Run(() => { if (_flag == 5) { Console.WriteLine("Value: {0}", _value); } }); 

如果运行t1和t2,那么结果就是没有输出或者“Value:10”。 可能是编译器在t1函数内切换行。 如果t2执行,则可能是_flag的值为5,但是_value的值为0.因此,预期的逻辑可能被破坏。

要解决这个问题,你可以使用可以应用到这个字段的volatile关键字。 这个语句禁用编译器优化,所以你可以在你的代码中强制正确的顺序。

 private static volatile int _flag = 0; 

只有在真正需要时才应该使用volatile ,因为它会禁用某些编译器优化,会损害性能。 它也不被所有.NET语言支持(Visual Basic不支持它),所以它妨碍了语言的互操作性。

多个线程可以访问一个variables。 最新的更新将在variables上