有人能很好地解释一下c#中的volatile关键字吗?它能解决哪些问题,不能解决哪些问题?在哪些情况下,它将节省我使用锁定?


当前回答

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

其他回答

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

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

我发现Joydip Kanjilal的这篇文章非常有用!

当您将一个对象或变量标记为volatile时,它将成为volatile读写的候选对象。需要注意的是,在c#中,所有的内存写操作都是volatile的,不管你写的是volatile对象还是非volatile对象。但是,当读取数据时,就会出现这种不确定性。当读取非易失性数据时,执行线程可能总是获得最新的值,也可能不总是。如果对象是volatile,线程总是获得最新的值

我就把它放在这里,供大家参考

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

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

你可以把volatile关键字看作是对编译器说“我想让你把这个值存储在内存中”。这保证了第二个线程检索到最新的值。

如果你想稍微了解一下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;
}

使用标准的优化(发布)编译器设置,编译器创建以下汇编器(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              

Looking at the output, the compiler has decided to use the ecx register to store the value of the j variable. For the non-volatile loop (the first) the compiler has assigned i to the eax register. Fairly straightforward. There are a couple of interesting bits though - the lea ebx,[ebx] instruction is effectively a multibyte nop instruction so that the loop jumps to a 16 byte aligned memory address. The other is the use of edx to increment the loop counter instead of using an inc eax instruction. The add reg,reg instruction has lower latency on a few IA32 cores compared to the inc reg instruction, but never has higher latency.

Now for the loop with the volatile loop counter. The counter is stored at [esp] and the volatile keyword tells the compiler the value should always be read from/written to memory and never assigned to a register. The compiler even goes so far as to not do a load/increment/store as three distinct steps (load eax, inc eax, save eax) when updating the counter value, instead the memory is directly modified in a single instruction (an add mem,reg). The way the code has been created ensures the value of the loop counter is always up-to-date within the context of a single CPU core. No operation on the data can result in corruption or data loss (hence not using the load/inc/store since the value can change during the inc thus being lost on the store). Since interrupts can only be serviced once the current instruction has completed, the data can never be corrupted, even with unaligned memory.

Once you introduce a second CPU to the system, the volatile keyword won't guard against the data being updated by another CPU at the same time. In the above example, you would need the data to be unaligned to get a potential corruption. The volatile keyword won't prevent potential corruption if the data cannot be handled atomically, for example, if the loop counter was of type long long (64 bits) then it would require two 32 bit operations to update the value, in the middle of which an interrupt can occur and change the data.

因此,volatile关键字只适用于小于或等于本机寄存器大小的对齐数据,这样操作总是原子的。

volatile关键字被设想用于IO操作,其中IO将不断变化,但有一个恒定的地址,例如内存映射的UART设备,编译器不应该一直重用从地址中读取的第一个值。

如果要处理大数据或有多个cpu,则需要更高级别(OS)的锁定系统来正确处理数据访问。