Vários textos dizem que, ao implementar o bloqueio com verificação dupla no .NET, o campo que você está bloqueando deve ter o 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 "lock (syncRoot)" não atinge a consistência de memória necessária? Não é verdade que após a instrução de "bloqueio" tanto a leitura quanto a gravação seriam voláteis e, portanto, a consistência necessária seria alcançada?
Respostas:
Volátil é desnecessário. Bem, tipo de **
volatile
é usado para criar uma barreira de memória * entre leituras e gravações na variável.lock
, quando usado, faz com que sejam criadas barreiras de memória ao redor do bloco dentro dolock
, além de limitar o acesso ao bloco a um thread.As barreiras de memória 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 reordene as instruções. Usar
volatile
é desnecessário ** porque você já tem um cadeado.Joseph Albahari explica essas coisas muito melhor do que eu jamais poderia.
E certifique-se de verificar o guia de Jon Skeet para implementar o singleton em C #
update :
*
volatile
faz com que as leituras da variável sejam seVolatileRead
as gravações sejamVolatileWrite
s, que em x86 e x64 em CLR, são implementadas com aMemoryBarrier
. Eles podem ser mais refinados em outros sistemas.** minha resposta está correta apenas se você estiver usando o CLR em processadores x86 e x64. Ele pode ser verdade em outros modelos de memória, como em Mono (e outras implementações), Itanium64 e hardware futuro. Isso é o que Jon está se referindo em seu artigo nas "pegadinhas" para bloqueio com verificação dupla.
Pode ser necessário fazer um dos {marcar a variável como
volatile
, lê-la comThread.VolatileRead
ou inserir uma chamada paraThread.MemoryBarrier
} para que o código funcione corretamente em uma situação de modelo de memória fraca.Pelo que entendi, no CLR (mesmo no IA64), as gravações nunca são reordenadas (as gravações sempre têm semântica de liberação). No entanto, no IA64, as leituras podem ser reordenadas para vir antes das gravações, a menos que sejam marcadas como voláteis. Infelizmente, não tenho acesso ao hardware IA64 para brincar, então qualquer coisa que eu diga 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á vinculado a isso, ele fala sobre bloqueio com verificação dupla)
artigo de chris brumme (tudo está vinculado a este )
Joe Duffy: Variantes quebradas de bloqueio duplo verificado
A série de luis abreu sobre multithreading também oferece uma boa 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
fonte
volatile
era desnecessário em qualquer plataforma, em seguida, isso significaria o JIT não poderia otimizar as cargas de memóriaobject s1 = syncRoot; object s2 = syncRoot;
paraobject s1 = syncRoot; object s2 = s1;
nessa plataforma. Isso parece muito improvável para mim.Existe uma maneira de implementá-lo sem
volatile
campo. Eu vou explicar isso ...Acho que é o reordenamento do acesso à memória dentro do bloqueio que é perigoso, de modo que você pode obter uma instância não totalmente inicializada fora do bloqueio. Para evitar isso, eu faço o seguinte:
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; } } }
Compreender o código
Imagine que haja algum código de inicialização dentro do construtor da classe Singleton. Se essas instruções forem reordenadas depois que o campo for definido com o endereço do novo objeto, você terá uma instância incompleta ... imagine que a classe tem este 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 estas operações:
ptr = allocate memory for Singleton; set ptr._value to 1; set Singleton.instance to ptr;
E se eu reordenar 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 tópico. SIM, se você pensar em vários tópicos ... e se o tópico 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 é o que a barreira da memória evita, ao não permitir o reordenamento do acesso à memória:
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;
Boa codificação!
fonte
new
está totalmente inicializado ..Acho que ninguém realmente respondeu à pergunta , então 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 threads.Portanto, a questão é: por que você adicionaria o primeiro
if (instance == null)
?A razão é presumivelmente para evitar a execução desnecessária da seção bloqueada do código. Enquanto você está executando o código dentro do bloqueio, qualquer outro thread que tente também executar esse código é bloqueado, o que tornará seu programa lento se você tentar acessar o singleton com freqüência de muitos threads. Dependendo do idioma / plataforma, também pode haver sobrecargas do próprio bloqueio que você deseja evitar.
Portanto, a primeira verificação de nulo é adicionada como uma maneira realmente rápida de ver se você precisa do bloqueio. Se você não precisa criar o singleton, pode evitar o bloqueio totalmente.
Mas você não pode verificar se a referência é nula sem bloqueá-la de alguma forma, porque devido ao cache do processador, outra thread poderia alterá-la e você leria um valor "obsoleto" que levaria você a inserir o bloqueio desnecessariamente. Mas você está tentando evitar um bloqueio!
Portanto, você torna o singleton volátil para garantir a leitura do valor mais recente, sem a necessidade de usar um bloqueio.
Você ainda precisa do bloqueio interno porque o volátil o protege apenas durante um único acesso à 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 Singleton.Instance pode causar ineficiência devido aos bloqueios, por que você está ligando para ele com tanta frequência que isso seria um problema significativo ? O ponto principal de um singleton é que há apenas um, então seu código pode ler e armazenar em cache a referência do singleton uma vez.
O único caso em que consigo pensar em que esse armazenamento em cache não seria possível seria quando você tivesse um grande número de threads (por exemplo, um servidor usando um novo thread para processar cada solicitação poderia estar criando milhões de threads de execução muito curta, cada um que teria que chamar Singleton.Instance uma vez).
Então, eu suspeito que o bloqueio de dupla verificação é um mecanismo que tem um lugar real em casos muito específicos de desempenho crítico, e então todo mundo subiu no movimento "esta é a maneira correta de fazer isso" sem realmente pensar o que ele faz e se será realmente necessário caso eles o estejam usando.
fonte
volatile
não tem nada a ver com a semântica de bloqueio no bloqueio de verificação dupla, tem a ver com o modelo de memória e coerência do cache. Seu objetivo é garantir que um thread não receba um valor que ainda está sendo inicializado por outro thread, o que o padrão de bloqueio de verificação dupla não impede inerentemente. Em Java, você definitivamente precisa davolatile
palavra - chave; no .NET é obscuro, porque está errado de acordo com ECMA, mas certo de acordo com o tempo de execução. De qualquer forma, olock
definitivamente não cuida disso.lock
torna o código seguro para o segmento. Essa parte é verdade, mas o padrão de bloqueio de verificação dupla pode torná-lo inseguro . É isso que você parece estar perdendo. Esta resposta parece vagar sobre o significado e o propósito de um bloqueio de verificação dupla sem nunca abordar os problemas de segurança de thread que são o motivovolatile
.instance
estiver marcado comvolatile
?Você deve usar volatile 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 voláteis: https://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10
Mas eles falham em ler até o fim: " Uma palavra final de advertência - estou apenas supondo o modelo de memória x86 a partir do comportamento observado nos processadores existentes. Portanto, as técnicas de baixo bloqueio também são frágeis porque o hardware e os compiladores podem ficar mais agressivos com o tempo . Aqui estão algumas estratégias para minimizar o impacto dessa fragilidade em seu código. Primeiro, sempre que possível, evite técnicas de low-lock. (...) Por fim, assuma o modelo de memória mais fraco possível, usando declarações voláteis em vez de confiar em garantias implícitas . "
Se você precisar de mais convencimento, leia este artigo sobre a especificação ECMA que será usada para outras plataformas: msdn.microsoft.com/en-us/magazine/jj863136.aspx
Se você precisar de mais convicção, leia este artigo mais recente de que otimizações podem ser feitas para evitar que ele funcione sem voláteis: msdn.microsoft.com/en-us/magazine/jj883956.aspx
Em resumo, ele "pode" funcionar para você sem voláteis no momento, mas não se arrisque a escrever o código adequado e usar os métodos volátil ou de leitura / gravação volátil. Artigos que sugerem fazer o contrário às vezes estão deixando de fora alguns dos possíveis riscos de otimizações de JIT / compilador que podem afetar seu código, bem como otimizações futuras que podem acontecer e podem quebrar seu código. Além disso, conforme as premissas mencionadas no último artigo, as premissas anteriores de trabalhar sem voláteis podem não se manter no ARM.
fonte
AFAIK (e - tome cuidado, não estou fazendo muitas coisas simultâneas) não. O bloqueio fornece apenas a sincronização entre vários contendores (threads).
volátil, por outro lado, diz à sua máquina para reavaliar o valor todas as vezes, para que você não tropece em um valor armazenado em cache (e errado).
Consulte http://msdn.microsoft.com/en-us/library/ms998558.aspx e observe a seguinte citação:
Uma descrição de volátil: http://msdn.microsoft.com/en-us/library/x13ttww7%28VS.71%29.aspx
fonte
Acho que encontrei o que procurava. Os detalhes estão neste artigo - http://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10 .
Para resumir - em .NET o modificador volátil não é realmente necessário nesta situação. No entanto, em modelos de memória mais fracos, as gravações feitas no construtor do objeto iniciado lentamente podem ser atrasadas após a gravação no campo, de modo que outros threads podem ler uma instância não nula corrompida na primeira instrução if.
fonte
O
lock
é suficiente. A própria especificação de linguagem MS (3.0) menciona este cenário exato em §8.12, sem qualquer menção devolatile
:fonte
Esta é uma postagem muito boa sobre o uso de volatile com bloqueio de verificação dupla:
http://tech.puredanger.com/2007/06/15/double-checked-locking/
Em Java, se o objetivo é proteger uma variável, você não precisa bloquear se ela estiver marcada como volátil
fonte