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

299

Alguém pode fornecer uma boa explicação para a palavra-chave volátil em c #? Quais problemas ele resolve e quais não? Em quais casos me poupará o uso do bloqueio?

Doron Yaacoby
fonte
6
Por que você deseja economizar no uso do bloqueio? Bloqueios não ordenados adicionam alguns nanossegundos ao seu programa. Você realmente não pode pagar alguns nanossegundos?
Eric Lippert

Respostas:

273

Acho que não há uma pessoa melhor para responder a isso do que Eric Lippert (ênfase no original):

No C #, "volátil" significa não apenas "garantir que o compilador e o jitter não executem nenhuma reordenação de código ou registrem otimizações de cache nessa variável". Também significa "instrua os processadores a fazer o que for necessário para garantir que eu esteja lendo o valor mais recente, mesmo que isso signifique interromper outros processadores e fazê-los sincronizar a memória principal com seus caches".

Na verdade, esse último pedaço é uma mentira. A verdadeira semântica das leituras e gravações voláteis é consideravelmente mais complexa do que descrevi aqui; na verdade, eles não garantem que todo processador interrompa o que está fazendo e atualiza os caches para / da memória principal. Em vez disso, fornecem garantias mais fracas sobre como os acessos à memória antes e depois das leituras e gravações podem ser observados como ordenados entre si . Certas operações, como criar um novo encadeamento, inserir um bloqueio ou usar um dos métodos da família de Intertravados, introduzem garantias mais fortes sobre a observação de pedidos. Se você quiser obter mais detalhes, leia as seções 3.10 e 10.5.3 da especificação C # 4.0.

Sinceramente, desencorajo você a criar um campo volátil . Campos voláteis são um sinal de que você está fazendo algo absolutamente louco: você está tentando ler e escrever o mesmo valor em dois threads diferentes sem colocar um bloqueio. Os bloqueios garantem que a memória lida ou modificada dentro do bloqueio seja observada consistente, os bloqueios garantem que apenas um encadeamento acesse um determinado pedaço de memória por vez e assim por diante. O número de situações em que um bloqueio é muito lento é muito pequeno e a probabilidade de você errar o código porque não entende o modelo exato de memória é muito grande. Não tento escrever nenhum código de bloqueio baixo, exceto para os usos mais triviais das operações intertravadas. Deixo o uso de "volátil" para especialistas reais.

Para uma leitura mais detalhada, consulte:

Ohad Schneider
fonte
29
Eu votaria para baixo se pudesse. Há muitas informações interessantes, mas isso realmente não responde à pergunta dele. Ele está perguntando sobre o uso da palavra-chave volátil no que se refere ao bloqueio. Por um bom tempo (antes do 2.0 RT), a palavra-chave volátil era necessária para usar corretamente para tornar um encadeamento de campo estático seguro se a instância de campo tivesse algum código de inicialização no construtor (consulte a resposta de AndrewTek). Ainda existe muito código 1.1 RT nos ambientes de produção e os desenvolvedores que o mantêm devem saber por que essa palavra-chave existe e se é seguro removê-lo.
Paul Easter
3
@PaulEaster: o fato de poder ser usado para bloqueio verificado por doulbe (geralmente no padrão singleton) não significa que deveria . Confiar no modelo de memória .NET provavelmente é uma prática ruim - você deve confiar no modelo ECMA. Por exemplo, você pode querer portar para mono um dia, que pode ter um modelo diferente. Também devo entender que diferentes arquiteturas de hardware podem mudar as coisas. Para mais informações, consulte: stackoverflow.com/a/7230679/67824 . Para melhores alternativas únicas (para todas as versões do .NET) veja: csharpindepth.com/articles/general/singleton.aspx
Ohad Schneider
6
Em outras palavras, a resposta correta para a pergunta é: Se o seu código estiver sendo executado no tempo de execução 2.0 ou posterior, a palavra-chave volátil quase nunca será necessária e causará mais danos do que benefícios se usada desnecessariamente. Mas nas versões anteriores do tempo de execução, é necessário o bloqueio adequado de verificação dupla nos campos estáticos.
Paul Easter
3
isso significa que bloqueios e variáveis ​​voláteis são mutuamente exclusivas no seguinte sentido: se eu usei bloqueios em torno de alguma variável, não há mais necessidade de declarar essa variável como volátil?
Giorgim
4
@Giorgi sim - as barreiras de memória garantidos por volatileestarão lá em virtude do bloqueio
Ohad Schneider
54

Se você quiser ser 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 padrão do compilador otimizado (versão), 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              

Observando a saída, o compilador decidiu usar o registro ecx para armazenar o valor da variável j. Para o loop não volátil (o primeiro), o compilador atribuiu i ao registro eax. Bastante direto. No entanto, existem alguns bits interessantes - a instrução lea ebx, [ebx] é efetivamente uma instrução nib multibyte, de modo que o loop salta para um endereço de memória alinhado de 16 bytes. O outro é o uso de edx para incrementar o contador de loop em vez de usar uma instrução inc eax. A instrução add reg, reg possui menor latência em alguns núcleos IA32 em comparação com a instrução inc reg, mas nunca possui latência mais alta.

Agora, para o loop com o contador de loop volátil. O contador é armazenado em [esp] e a palavra-chave volátil informa ao compilador que o valor deve sempre ser lido / gravado na memória e nunca atribuído a um registro. O compilador chega ao ponto de não fazer um carregamento / incremento / armazenamento em três etapas distintas (carregar eax, inc eax, salvar eax) ao atualizar o valor do contador, em vez disso, a memória é modificada diretamente em uma única instrução (um add mem , reg). A maneira como o código foi criado garante que o valor do contador de loop 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 é possível usar a carga / inc / armazenamento, pois o valor pode mudar durante o inc, perdendo assim no armazenamento). Como as interrupções só podem ser reparadas após a conclusão da instrução atual,

Depois de introduzir uma segunda CPU no sistema, a palavra-chave volátil não se protegerá contra a atualização dos dados por outra CPU ao mesmo tempo. No exemplo acima, você precisaria que os dados estivessem desalinhados para obter uma possível corrupção. A palavra-chave volátil não impedirá a corrupção em potencial se os dados não puderem ser manipulados atomicamente, por exemplo, se o contador de loop for do tipo longo (64 bits), serão necessárias duas operações de 32 bits para atualizar o valor, no meio de qual uma interrupção pode ocorrer e alterar os dados.

Portanto, a palavra-chave volátil é boa apenas para dados alinhados que são menores ou iguais ao tamanho dos registros nativos, de modo que as operações sejam sempre atômicas.

A palavra-chave volátil foi concebida para ser usada com operações de E / S nas quais o IO mudava constantemente, mas tinha um endereço constante, como um dispositivo UART mapeado na memória, 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 superior (SO) para lidar com o acesso a dados corretamente.

Skizz
fonte
Isso é C ++, mas o princípio se aplica ao C #.
Skizz 16/09/08
6
Eric Lippert escreve que o volátil no C ++ apenas impede que o compilador realize algumas otimizações, enquanto no C # o volátil também faz alguma comunicação entre os outros núcleos / processadores para garantir que o valor mais recente seja lido.
Peter Huber
44

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

  1. O segmento 1 pergunta se uma variável é nula. //if(this.foo == null)
  2. O segmento 1 determina que a variável é nula e, portanto, insere um bloqueio. //lock(this.bar)
  3. O segmento 1 pergunta novamente se a variável é nula. //if(this.foo == null)
  4. O segmento 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, o this.foo podia ser atribuído à nova instância do Foo, antes da execução da execução do construtor. Nesse caso, um segundo thread pode entrar (durante a chamada do thread 1 para o construtor do 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, portanto, tenta usá-la. //this.foo.MakeFoo ()

Antes do .NET 2.0, você poderia declarar this.foo como volátil para solucionar esse problema. Desde o .NET 2.0, você não precisa mais usar a palavra-chave volátil para realizar o bloqueio com verificação dupla.

A Wikipedia, na verdade, tem um bom artigo sobre o bloqueio verificado duplo e aborda brevemente este tópico: http://en.wikipedia.org/wiki/Double-checked_locking

AndrewTek
fonte
2
isso é exatamente o que vejo em um código legado e fiquei pensando sobre isso. por isso iniciei uma pesquisa mais profunda. Obrigado!
Peter Porfy
1
Não entendo como o Thread 2 atribuiria valor foo? O encadeamento 1 não está bloqueado this.bare, portanto, apenas o encadeamento 1 poderá inicializar o foo em um determinado momento? Quero dizer, você verificar o valor após o bloqueio será liberado novamente, quando de qualquer maneira ele deve ter o novo valor a partir de fios 1.
gilmishal
24

Às vezes, o compilador otimiza um campo e usa um registro para armazená-lo. Se o thread 1 gravar no campo e outro acessar, uma vez que a atualização foi armazenada em um registro (e não na memória), o segundo thread obterá 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 thread recupere o valor mais recente.

Benoit
fonte
21

Do MSDN : o modificador volátil geralmente é usado para um campo que é acessado por vários threads sem usar a instrução lock para serializar o acesso. O uso do modificador volátil garante que um thread recupere o valor mais atualizado gravado por outro thread.

Dr. Bob
fonte
13

O CLR gosta de otimizar as instruções; portanto, quando você acessa um campo no código, ele nem sempre pode acessar o valor atual do campo (pode ser da pilha, etc.). Marcar um campo como volatilegarante 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 no 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.

Joseph Daigle
fonte
3

Eu encontrei este artigo por Joydip Kanjilal muito útil!

When you mark an object or a variable as volatile, it becomes a candidate for volatile reads and writes. It should be noted that in C# all memory writes are volatile irrespective of whether you are writing data to a volatile or a non-volatile object. However, the ambiguity happens when you are reading data. When you are reading data that is non-volatile, the executing thread may or may not always get the latest value. If the object is volatile, the thread always gets the most up-to-date value

Vou deixar aqui para referência

Christina Katsakoula
fonte
0

À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 thread único, mas pode ser um problema no ambiente de vários threads. 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, não esperaria saída ou "Valor: 10" como resultado. Pode ser que o compilador alterne a 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 palavras-chave voláteis que podem ser aplicadas ao campo. Esta declaração desativa 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 o volátil apenas se realmente precisar, pois, se desabilitar certas otimizações do compilador, isso prejudicará o desempenho. Também não é suportado por todas as linguagens .NET (o Visual Basic não é compatível), portanto dificulta a interoperabilidade da linguagem.

Aliaksei Maniuk
fonte
2
Seu exemplo é muito ruim. O programador nunca deve ter nenhuma expectativa sobre o valor de _flag na tarefa t2 com base no fato de que o código de t1 é escrito primeiro. Escrito primeiro! = Executado primeiro. Não importa se o compilador alterna essas duas linhas em t1. Mesmo que o compilador não tenha alternado essas instruções, seu Console.WriteLne no ramo else ainda poderá ser executado, mesmo com a palavra-chave volátil em _flag.
Jakotheshadows 8/16
@jakotheshadows, você está certo, eu editei minha resposta. A minha ideia principal era mostrar que a lógica esperado pode ser quebrado quando corremos t1 e t2 simultaneamente
Aliaksei Maniuk
0

Portanto, para resumir tudo isso, a resposta correta para a pergunta é: se o seu código estiver sendo executado no tempo de execução 2.0 ou posterior, a palavra-chave volátil quase nunca será necessária e causará mais danos do que benefícios se usada desnecessariamente. IE Nunca o use. MAS, nas versões anteriores do tempo de execução, é necessário o bloqueio adequado de verificação dupla nos campos estáticos. Campos especificamente estáticos cuja classe possui código de inicialização de classe estática.

Paul Easter
fonte
-4

vários threads podem acessar uma variável. A atualização mais recente estará na variável

user2943601
fonte