Será Interlocked fornecer visibilidade em todos os tópicos?
-
21-08-2019 - |
Pergunta
Suponha que eu tenho um "contra" variável, e há vários segmentos acessar e ajustar o valor de "balcão" usando Encaixado, ou seja:.
int value = Interlocked.Increment(ref counter);
e
int value = Interlocked.Decrement(ref counter);
Posso assumir que, a mudança feita por Interlocked será visível em todos os tópicos?
Se não, o que devo fazer para que os tópicos sincronizar a variável?
EDIT: alguém me sugeriu usar volátil. Mas quando eu definir o "balcão" tão volátil, não há aviso do compilador "com referência ao campo volátil não será tratada como volátil".
Quando li ajuda on-line, ele disse: "Um campo volátil não deve normalmente ser passado usando um ref ou out parâmetro".
Solução
InterlockedIncrement / Decrement em x86 CPUs (de x86 bloqueio add / dec) estão criando automaticamente barreira de memória que dá visibilidade a todos os tópicos (ou seja, os tópicos podem ver sua atualização como em ordem, como seqüencial consistência de memória). barreira de memória faz com que todos os pendentes cargas de memória / lojas para ser concluída. volatile
não está relacionado com esta questão, embora C # e Java (e alguns compiladores C / C ++) fazer cumprir volatile
para fazer barreira de memória. Mas, operação interligada já tem barreira de memória, CPU.
Por favor, também dar uma olhada minha outra resposta ??a > em stackoverflow.
Note que eu tenho supor que C # 's InterlockedIncrement / Decrement são mapeamento intrínseco para adicionar bloqueio de x86 / DEC.
Outras dicas
Posso assumir que, a mudança feita por Interlocked será visível em todos os tópicos?
Isso depende de como você lê o valor. Se você "apenas" lê-lo, então não, isso não vai ser sempre visível em outros tópicos, a menos que marcá-lo como volátil. Isso faz com que um aviso irritante embora.
Como uma alternativa (e muito preferido IMO), lê-lo usando outra instrução Interlocked. Isso sempre vai ver o valor atualizado sobre os tópicos:
int readvalue = Interlocked.CompareExchange(ref counter, 0, 0);
que retorna o valor lido, e se era 0 swaps de TI com 0.
Motivação: as dicas de alerta de que algo não está certo; combinando as duas técnicas (volátil e intertravado) não era o caminho a intenção de fazer isso.
Update: parece que uma outra abordagem para confiável 32-bit lê sem usar "volátil" é usando Thread.VolatileRead
como sugerido na esta resposta . Há também algumas evidências de que estou completamente errado sobre o uso Interlocked
para 32-bit lê, por exemplo, este Ligue questão , embora eu me pergunto se a distinção é um pedante pouco na natureza.
O que eu realmente quero dizer é: não use esta resposta como sua única fonte; Eu estou tendo minhas dúvidas sobre isso.
Na verdade, eles não são. Se você deseja modificar com segurança counter
, então você está fazendo a coisa correta. Mas se você quiser ler counter
diretamente você precisa declará-lo como volatile
. Caso contrário, o compilador não tem nenhuma razão para acreditar que counter
vai mudar porque as operações Interlocked
estão em código que ele não pode ver.
garante bloqueadas que apenas 1 thread por vez pode atualizar o valor. Para garantir que outros segmentos podem ler o valor correto (e não um valor em cache) marcá-lo como volátil.
int volátil pública Contador;
Não; um Interlocked-at-Write-Only sozinho faz não garantir que variável lê no código são realmente fresco; um programa que não lê corretamente a partir de um campo bem pode não ser thread-safe , mesmo sob um "modelo de memória forte". Isto aplica-se a qualquer forma de atribuir a um campo compartilhado entre threads.
Aqui está um exemplo de código que nunca terminará devido ao JIT . (Foi modificada a partir barreiras de memória em. NET para ser um programa LINQPad executável atualizado para a questão).
// Run this as a LINQPad program in "Release Mode".
// ~ It will never terminate on .NET 4.5.2 / x64. ~
// The program will terminate in "Debug Mode" and may terminate
// in other CLR runtimes and architecture targets.
class X {
// Adding {volatile} would 'fix the problem', as it prevents the JIT
// optimization that results in the non-terminating code.
public int terminate = 0;
public int y;
public void Run() {
var r = new ManualResetEvent(false);
var t = new Thread(() => {
int x = 0;
r.Set();
// Using Volatile.Read or otherwise establishing
// an Acquire Barrier would disable the 'bad' optimization.
while(terminate == 0){x = x * 2;}
y = x;
});
t.Start();
r.WaitOne();
Interlocked.Increment(ref terminate);
t.Join();
Console.WriteLine("Done: " + y);
}
}
void Main()
{
new X().Run();
}
A explicação de barreiras de memória em .NET :
Desta vez é JIT, e não o hardware. É claro que JIT tem em cache o valor da variável de terminar [no EAX registrar e do] programa está agora preso no laço destacado acima. .
De qualquer usando um
lock
ou adicionando umThread.MemoryBarrier
dentro do loop while irá corrigir o problema. Ou você ainda pode usarVolatile.Read
[ou um campovolatile
]. O objetivo da barreira de memória aqui é apenas para otimizações JIT suprimir. Agora que vimos como software e hardware pode reordenar operações de memória , é hora de discutir barreiras de memória ..
Isto é, um adicional de barreira construção é necessária no lado da leitura para evitar problemas com compilação e JIT re-ordenação / otimizações : esta é uma questão diferente do que a coerência de memória!
Adicionando volatile
aqui faria evitar a otimização JIT, e, assim, 'resolver o problema', mesmo que tais resultados em um aviso. Este programa também pode ser corrigido através do uso de Volatile.Read
ou uma das várias outras operações que causam uma barreira:. Estas barreiras são tanto uma parte do programa de correção CLR / JIT como as cercas de memória hardware subjacente