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?
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?
Respostas:
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:
volatile
estarão lá em virtude do bloqueio
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.
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:
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:
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
foo
? O encadeamento 1 não está bloqueado this.bar
e, 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.
À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.
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 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 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.
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
À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.
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.
vários threads podem acessar uma variável. A atualização mais recente estará na variável