it-swarm-pt.tech

Quando a palavra-chave volátil deve ser usada em C #?

Alguém pode fornecer uma boa explicação da palavra-chave volátil em c #? Quais problemas são resolvidos e quais não? Em quais casos me poupará o uso de bloqueio?

279
Doron Yaacoby

Se você quiser ficar um pouco mais técnico sobre o que a palavra-chave volátil faz, considere o seguinte programa (estou usando o 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;
}

Usando as configurações de compilador otimizado padrão (release), o compilador cria o seguinte assembler (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              

Olhando para a saída, o compilador decidiu usar o registrador ecx para armazenar o valor da variável j. Para o loop não volátil (o primeiro), o compilador atribuiu i ao registrador eax. Razoavelmente direto. No entanto, existem alguns bits interessantes - a instrução ebx [lea ebx] é efetivamente uma instrução nop multibyte para que o loop salte para um endereço de memória alinhado de 16 bytes. O outro é o uso de edx para incrementar o contador de loops ao invés de usar uma instrução inc ex. O comando add reg, reg tem menor latência em alguns núcleos IA32 em comparação com a instrução inc reg, mas nunca tem maior latência. 

Agora para o loop com o contador de loop volátil. O contador é armazenado em [esp] e a palavra-chave volatile informa ao compilador que o valor sempre deve ser lido/gravado na memória e nunca atribuído a um registrador. O compilador vai tão longe a ponto de não fazer um load/increment/store como três etapas distintas (load eax, inc eax, save eax) ao atualizar o valor do contador, ao invés disso a memória é modificada diretamente em uma única instrução , reg). A maneira como o código foi criado garante que o valor do contador de loops esteja sempre atualizado no contexto de um único núcleo da CPU. Nenhuma operação nos dados pode resultar em corrupção ou perda de dados (portanto, não usar o load/inc/store, pois o valor pode ser alterado durante o inc, sendo assim perdido na loja). Como as interrupções só podem ser atendidas após a conclusão da instrução atual, os dados nunca poderão ser corrompidos, mesmo com memória não alinhada.

Uma vez introduzida uma segunda CPU no sistema, a palavra-chave volátil não protege contra os dados que estão sendo atualizados por outra CPU ao mesmo tempo. No exemplo acima, você precisaria que os dados fossem desalinhados para obter uma possível corrupção. A palavra-chave volátil não impedirá possíveis danos se os dados não puderem ser manipulados atomicamente, por exemplo, se o contador de loops for do tipo longo longo (64 bits), então serão necessárias duas operações de 32 bits para atualizar o valor, no meio que uma interrupção pode ocorrer e alterar os dados.

Portanto, a palavra-chave volátil só é válida para dados alinhados que seja menor ou igual ao tamanho dos registradores nativos, de modo que as operações sejam sempre atômicas.

A palavra-chave volátil foi concebida para ser usada com IO operações onde o IO estaria em constante mudança, mas tinha um endereço constante, como uma memória mapeada UART device, e o compilador não deve continuar reutilizando o primeiro valor lido do endereço.

Se você estiver lidando com dados grandes ou tiver várias CPUs, precisará de um sistema de bloqueio de nível mais alto (SO) para manipular o acesso aos dados corretamente.

54
Skizz

Se você estiver usando o .NET 1.1, a palavra-chave volátil será necessária ao fazer o bloqueio duplo verificado. Por quê? Como antes do .NET 2.0, o cenário a seguir poderia fazer com que um segundo thread acessasse um objeto não nulo, ainda que não totalmente construído:

  1. O thread 1 pergunta se uma variável é nula. // if (this.foo == null)
  2. O encadeamento 1 determina que a variável é nula, portanto, insere um bloqueio. // lock (this.bar)
  3. O thread 1 pergunta novamente se a variável é nula. // if (this.foo == null)
  4. O encadeamento 1 ainda determina que a variável é nula, portanto chama um construtor e atribui o valor à variável. // this.foo = new Foo ();

Antes do .NET 2.0, this.foo poderia ser atribuído à nova instância do Foo, antes que o construtor fosse finalizado. Nesse caso, um segundo thread pode entrar (durante a chamada do thread 1 para o construtor de Foo) e experimentar o seguinte:

  1. O segmento 2 pergunta se a variável é nula. // if (this.foo == null)
  2. O segmento 2 determina que a variável NÃO é nula, por isso tenta usá-la. // this.foo.MakeFoo ()

Antes do .NET 2.0, você poderia declarar this.foo como sendo volátil para contornar este problema. Desde o .NET 2.0, você não precisa mais usar a palavra-chave volátil para realizar o bloqueio duplo verificado.

A Wikipedia, na verdade, tem um bom artigo sobre o Double Checked Locking, e toca brevemente neste tópico: http://en.wikipedia.org/wiki/Double-checked_locking

38
AndrewTek

De MSDN : O modificador volátil é geralmente usado para um campo que é acessado por vários encadeamentos sem usar a instrução de bloqueio para serializar o acesso. Usar o modificador volátil garante que um thread recupere o valor mais atualizado gravado por outro thread.

22
Dr. Bob

Às vezes, o compilador otimizará um campo e usará um registro para armazená-lo. Se o thread 1 fizer uma gravação no campo e outro thread acessá-lo, uma vez que a atualização foi armazenada em um registrador (e não na memória), o segundo thread obteria dados obsoletos.

Você pode pensar na palavra-chave volátil dizendo ao compilador "Quero que você armazene esse valor na memória". Isso garante que o segundo segmento recupera o valor mais recente.

20
Benoit

O CLR gosta de otimizar instruções, então quando você acessa um campo em código, ele nem sempre pode acessar o valor atual do campo (pode ser da pilha, etc). Marcar um campo como volatile garante que o valor atual do campo seja acessado pela instrução. Isso é útil quando o valor pode ser modificado (em um cenário sem bloqueio) por um encadeamento simultâneo em seu programa ou algum outro código em execução no sistema operacional.

Você obviamente perde alguma otimização, mas mantém o código mais simples.

13
Joseph Daigle

Às vezes, o compilador altera a ordem das instruções no código para otimizá-lo. Normalmente, isso não é um problema no ambiente de encadeamento único, mas pode ser um problema no ambiente multiencadeado. Veja o seguinte exemplo:

 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);
     }
 });

Se você executar t1 e t2, você não esperaria nenhuma saída ou "Valor: 10" como resultado. Pode ser que o compilador mude de linha dentro da função t1. Se t2 for executado, pode ser que _flag tenha valor 5, mas _value tenha 0. Portanto, a lógica esperada pode ser quebrada. 

Para corrigir isso, você pode usar a palavra-chave volatile que você pode aplicar ao campo. Essa instrução desabilita as otimizações do compilador para que você possa forçar a ordem correta no seu código.

private static volatile int _flag = 0;

Você deve usar volatile somente se realmente precisar dele, porque ele desabilita certas otimizações do compilador, isso prejudicará o desempenho. Também não é suportado por todas as linguagens .NET (o Visual Basic não suporta isso), por isso dificulta a interoperabilidade do idioma.

0
Aliaksei Maniuk

Então, para resumir tudo isso, a resposta correta para a pergunta é: Se seu código está rodando no tempo de execução 2.0 ou posterior, a palavra-chave volátil quase nunca é necessária e faz mais mal do que bem se usada desnecessariamente. I.E. Nunca use isso. MAS em versões anteriores do tempo de execução, é necessário IS para bloqueio de verificação dupla em campos estáticos. Especificamente campos estáticos cuja classe possui código de inicialização de classe estática.

0
Paul Easter