A necessidade de modificador volátil no bloqueio verificado em dobro no .NET

Múltiplos textos dizem que ao implementar o bloqueio duplo verificado no .NET, o campo que você está bloqueando deve ter um modificador volátil aplicado. Mas por que exatamente? Considerando o seguinte exemplo:

public sealed class Singleton { private static volatile Singleton instance; private static object syncRoot = new Object(); private Singleton() {} public static Singleton Instance { get { if (instance == null) { lock (syncRoot) { if (instance == null) instance = new Singleton(); } } return instance; } } } 

Por que o “bloqueio (syncRoot)” não realiza a consistência de memory necessária? Não é verdade que, após a declaração “lock”, tanto a leitura quanto a gravação seriam voláteis e, portanto, a consistência necessária seria cumprida?

Volátil é desnecessário. Bem, mais ou menos

volatile é usado para criar uma barreira de memory * entre leituras e gravações na variável.
lock , quando usado, faz com que as barreiras de memory sejam criadas em torno do bloco dentro do lock , além de limitar o access ao bloco a um encadeamento.
As barreiras de memory fazem com que cada thread leia o valor mais atual da variável (não um valor local armazenado em cache em algum registro) e que o compilador não reordena as instruções. Usar o volatile é desnecessário ** porque você já tem um bloqueio.

Joseph Albahari explica essa coisa muito melhor do que eu jamais poderia.

E não se esqueça de verificar o guia de Jon Skeet para implementar o singleton em c #

atualização :
* VolatileRead faz com que as leituras da variável VolatileRead escreva como VolatileWrite s, que em x86 e x64 no CLR, são implementadas com um MemoryBarrier . Eles podem ser mais finos em outros sistemas.

** minha resposta está correta apenas se você estiver usando o CLR em processadores x86 e x64. Pode ser verdade em outros modelos de memory, como no Mono (e outras implementações), Itanium64 e hardware futuro. Isto é o que Jon está se referindo em seu artigo nas “pegadinhas” para bloqueio duplo verificado.

Fazer um de {marcar a variável como volatile , lê-la com Thread.VolatileRead ou inserir uma chamada em Thread.MemoryBarrier } pode ser necessário para o código funcionar corretamente em uma situação de modelo de memory fraca.

Pelo que entendi, no CLR (mesmo no IA64), as gravações nunca são reordenadas (as gravações sempre têm semânticas de lançamento). No entanto, no IA64, as leituras podem ser reordenadas antes das gravações, a menos que sejam marcadas como voláteis. Infelizmente, eu não tenho access ao hardware IA64 para jogar, então qualquer coisa que eu disser sobre isso seria especulação.

Eu também achei estes artigos úteis:
http://www.codeproject.com/KB/tips/MemoryBarrier.aspx
artigo de vance morrison (tudo está relacionado a isso, fala sobre bloqueio duplo verificado)
Artigo de Chris Brumme (tudo está relacionado a isso)
Joe Duffy: Variantes quebradas do bloqueio duplo verificado

A série de luis abreu sobre multithreading também oferece uma visão geral dos conceitos
http://msmvps.com/blogs/luisabreu/archive/2009/06/29/multithreading-load-and-store-reordering.aspx
http://msmvps.com/blogs/luisabreu/archive/2009/07/03/multithreading-introducing-memory-fences.aspx

Existe uma maneira de implementá-lo sem campo volatile . Eu vou explicar isso …

Eu acho que é o reordenamento de access à memory dentro do bloqueio que é perigoso, de tal forma que você pode obter uma instância inicializada não completa fora do bloqueio. Para evitar isso eu faço isso:

 public sealed class Singleton { private static Singleton instance; private static object syncRoot = new Object(); private Singleton() {} public static Singleton Instance { get { // very fast test, without implicit memory barriers or locks if (instance == null) { lock (syncRoot) { if (instance == null) { var temp = new Singleton(); // ensures that the instance is well initialized, // and only then, it assigns the static variable. System.Threading.Thread.MemoryBarrier(); instance = temp; } } } return instance; } } } 

Entendendo o código

Imagine que há algum código de boot dentro do construtor da class Singleton. Se essas instruções forem reordenadas depois que o campo for definido com o endereço do novo object, você terá uma instância incompleta … imagine que a class tenha esse código:

 private int _value; public int Value { get { return this._value; } } private Singleton() { this._value = 1; } 

Agora imagine uma chamada para o construtor usando o novo operador:

 instance = new Singleton(); 

Isso pode ser expandido para essas operações:

 ptr = allocate memory for Singleton; set ptr._value to 1; set Singleton.instance to ptr; 

E se eu reorganizar essas instruções assim:

 ptr = allocate memory for Singleton; set Singleton.instance to ptr; set ptr._value to 1; 

Isso faz diferença? NÃO, se você pensar em um único segmento. SIM, se você pensar em vários threads … e se o thread for interrompido logo após set instance to ptr :

 ptr = allocate memory for Singleton; set Singleton.instance to ptr; -- thread interruped here, this can happen inside a lock -- set ptr._value to 1; -- Singleton.instance is not completelly initialized 

É isso que a barreira de memory evita, ao não permitir o reordenamento do access à memory:

 ptr = allocate memory for Singleton; set temp to ptr; // temp is a local variable (that is important) set ptr._value to 1; -- memory barrier... cannot reorder writes after this point, or reads before it -- -- Singleton.instance is still null -- set Singleton.instance to temp; 

Codificação feliz!

Eu não acho que alguém tenha realmente respondido a pergunta , então eu vou tentar.

O volátil e o primeiro if (instance == null) não são “necessários”. O bloqueio tornará este código seguro para thread.

Então a questão é: por que você adicionaria o primeiro if (instance == null) ?

A razão é presumivelmente evitar a execução desnecessária da seção trancada do código. Enquanto você estiver executando o código dentro do bloqueio, qualquer outro thread que tentar executar esse código também será bloqueado, o que atrasará o seu programa se você tentar acessar o singleton frequentemente a partir de vários threads. Dependendo do idioma / plataforma, também pode haver overheads do próprio bloqueio que você deseja evitar.

Assim, a primeira verificação nula é adicionada como uma maneira realmente rápida de ver se você precisa do bloqueio. Se você não precisa criar o singleton, você pode evitar o bloqueio completamente.

Mas você não pode verificar se a referência é nula sem bloqueá-la de alguma forma, porque devido ao cache do processador, outro thread poderia alterá-la e você iria ler um valor “obsoleto” que o levaria a inserir o bloqueio desnecessariamente. Mas você está tentando evitar um bloqueio!

Então você torna o singleton volátil para garantir que você leia o valor mais recente, sem precisar usar um bloqueio.

Você ainda precisa do bloqueio interno porque o volátil apenas o protege durante um único access à variável – você não pode testá-lo e configurá-lo com segurança sem usar um bloqueio.

Agora, isso é realmente útil?

Bem, eu diria “na maioria dos casos, não”.

Se o Singleton.Instance poderia causar ineficiência devido aos bloqueios, então por que você está ligando tão freqüentemente que isso seria um problema significativo ? O ponto principal de um singleton é que existe apenas um, portanto, seu código pode ler e armazenar em cache a referência singleton uma vez.

O único caso em que posso pensar onde este cache não seria possível seria quando você tem um grande número de threads (por exemplo, um servidor usando um novo thread para processar cada requisição poderia estar criando milhões de threads muito curtos, cada um dos que teria que chamar Singleton.Instance uma vez).

Então, eu suspeito que o bloqueio duplo verificado é um mecanismo que tem um lugar real em casos críticos de desempenho muito específicos, e então todo mundo tem escalado o bandwagon “esta é a maneira correta de fazê-lo” sem realmente pensar o que ele faz e se será realmente necessário no caso de eles estarem usando para.

AFAIK (e – tome isso com caucanvas, não estou fazendo muita coisa concorrente) não. O bloqueio só lhe dá a synchronization entre vários contendores (threads).

Volátil, por outro lado, diz à sua máquina para reavaliar o valor toda vez, para que você não tropeça em um valor armazenado em cache (e errado).

Consulte http://msdn.microsoft.com/en-us/library/ms998558.aspx e observe a seguinte citação:

Além disso, a variável é declarada como volátil para garantir que a atribuição à variável de instância seja concluída antes que a variável de instância possa ser acessada.

Uma descrição de volátil: http://msdn.microsoft.com/pt-br/library/x13ttww7%28VS.71%29.aspx

Você deve usar volátil com o padrão de bloqueio de verificação dupla.

A maioria das pessoas aponta para este artigo como prova de que você não precisa de material volátil: https://msdn.microsoft.com/pt-br/magazine/cc163715.aspx#S10

Mas eles não conseguem ler até o final: ” Uma palavra final de aviso – Estou apenas adivinhando o modelo de memory x86 do comportamento observado em processadores existentes. Assim, técnicas de baixo bloqueio também são frágeis porque hardware e compiladores podem ficar mais agressivos ao longo do tempo Aqui estão algumas estratégias para minimizar o impacto desta fragilidade em seu código: Primeiro, sempre que possível, evite técnicas de baixo bloqueio. (…) Finalmente, assuma o modelo de memory mais fraco possível, usando declarações voláteis em vez de confiar em garantias implícitas. “

Se você precisar de mais informações, leia este artigo sobre a especificação ECMA que será usada para outras plataformas: msdn.microsoft.com/pt-br/magazine/jj863136.aspx

Se você precisar de mais informações, leia este artigo mais recente no qual otimizações podem ser colocadas para evitar que ele funcione sem volatilidade: msdn.microsoft.com/pt-br/magazine/jj883956.aspx

Em resumo, “pode” funcionar para você sem volatilidade no momento, mas não o faça escrever código correto e use os methods volátil ou volatileread / write. Os artigos que sugerem fazer o contrário, algumas vezes, deixam de fora alguns dos possíveis riscos das otimizações do JIT / compilador que podem afetar seu código, assim como otimizações futuras que podem acontecer e que podem quebrar seu código. Além disso, como suposições mencionadas no último artigo, suposições anteriores de trabalho sem volatilidade já podem não se sustentar no ARM.

O lock é suficiente. A especificação de linguagem MS (3.0) em si menciona este cenário exato em §8.12, sem qualquer menção de volatile :

Uma abordagem melhor é sincronizar o access a dados estáticos, bloqueando um object estático privado. Por exemplo:

 class Cache { private static object synchronizationObject = new object(); public static void Add(object x) { lock (Cache.synchronizationObject) { ... } } public static void Remove(object x) { lock (Cache.synchronizationObject) { ... } } } 

Eu acho que encontrei o que estava procurando. Os detalhes estão neste artigo – http://msdn.microsoft.com/pt-br/magazine/cc163715.aspx#S10 .

Resumindo – no modificador volátil do .NET, de fato não é necessário nesta situação. No entanto, em modelos de memory mais fracos, as gravações feitas no construtor do object iniciado com lentidão podem ser atrasadas após a gravação no campo, portanto, outros encadeamentos podem ler a instância não nula corrompida na primeira instrução if.

Este é um post muito bom sobre o uso de volátil com duplo bloqueio verificado:

http://tech.puredanger.com/2007/06/15/double-checked-locking/

Em Java, se o objective é proteger uma variável, você não precisa bloquear se ela estiver marcada como volátil