何时在C#中使用volatile关键字?

时间:2020-03-05 18:55:57  来源:igfitidea点击:

谁能提供C#中volatile关键字的很好解释?它可以解决哪些问题,而哪些不能解决?在什么情况下可以节省锁定的使用时间?

解决方案

回答

CLR喜欢优化指令,因此当我们访问代码中的字段时,它可能并不总是访问该字段的当前值(它可能来自堆栈等)。将字段标记为"易失性"可确保指令访问该字段的当前值。当可以通过程序中的并发线程或者操作系统中运行的某些其他代码修改值(在非锁定情况下)时,此功能很有用。

我们显然会失去一些优化,但这确实使代码更简单。

回答

从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

查看输出,编译器已决定使用ecx寄存器存储j变量的值。对于非易失性循环(第一个),编译器已将i分配给eax寄存器。非常坦率的。尽管lea ebx,[ebx]指令实际上是多字节nop指令,但仍有几个有趣的位,因此循环跳至16字节对齐的内存地址。另一个是使用edx来增加循环计数器,而不是使用inc eax指令。与inc reg指令相比,add reg,reg指令在几个IA32内核上具有较低的延迟,但从来没有更高的延迟。

现在使用易失性循环计数器进行循环。计数器存储在[esp]处,而volatile关键字告诉编译器应始终从存储器中读取/写入该值,并且永远不要将其分配给寄存器。甚至在更新计数器值时,编译器甚至不做为三个不同的步骤(加载eax,inc eax,保存eax)进行加载/增量/存储,而是直接在一条指令中修改内存(添加内存) ,reg)。创建代码的方式可确保在单个CPU内核的上下文中循环计数器的值始终是最新的。对数据的任何操作都不会导致损坏或者数据丢失(因此,由于在inc期间可能会更改值,因此在存储上丢失,因此不使用加载/存储/存储)。由于仅在当前指令完成后才可以处理中断,因此即使内存未对齐,数据也永远不会损坏。

一旦在系统中引入了第二个CPU,volatile关键字将无法防止另一个CPU同时更新数据。在上面的示例中,我们将需要对数据进行对齐以获取潜在的损坏。如果无法以原子方式处理数据,那么volatile关键字将不会防止潜在的损坏,例如,如果循环计数器的类型为long long(64位),则它将需要两个32位操作来更新值,中间可能发生中断并更改数据。

因此,volatile关键字仅适用于小于或者等于本机寄存器大小的对齐数据,因此操作始终是原子的。

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

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