Digamos que uma classe tenha um public int counter
campo que seja acessado por vários threads. Isso int
é apenas incrementado ou decrementado.
Para incrementar esse campo, qual abordagem deve ser usada e por quê?
lock(this.locker) this.counter++;
,Interlocked.Increment(ref this.counter);
,- Mude o modificador de acesso de
counter
parapublic volatile
.
Agora que descobri volatile
, removi muitas lock
instruções e o uso de Interlocked
. Mas há uma razão para não fazer isso?
c#
multithreading
locking
volatile
interlocked
testemunho
fonte
fonte
Respostas:
Pior (na verdade não vai funcionar)
Como outras pessoas mencionaram, isso por si só não é realmente seguro. A questão
volatile
é que vários threads em execução em várias CPUs podem e armazenarão em cache os dados e reordenarão as instruções.Caso contrário
volatile
, e a CPU A incrementa um valor, a CPU B pode não ver esse valor incrementado até algum tempo depois, o que pode causar problemas.Se for
volatile
, isso apenas garante que as duas CPUs vejam os mesmos dados ao mesmo tempo. Isso não os impede de intercalar suas operações de leitura e gravação, que é o problema que você está tentando evitar.Segundo melhor:
É seguro fazer isso (desde que você lembre-se de
lock
qualquer outro lugar que acessarthis.counter
). Impede que outros threads executem qualquer outro código que é protegido porlocker
. O uso de bloqueios também evita os problemas de reordenação de várias CPUs como acima, o que é ótimo.O problema é que o bloqueio é lento e, se você reutilizar o
locker
em outro lugar que não esteja realmente relacionado, poderá acabar bloqueando os outros threads sem motivo.Melhor
Isso é seguro, pois efetivamente lê, incrementa e grava em 'um hit' que não pode ser interrompido. Por esse motivo, ele não afetará nenhum outro código, e você também não precisa se lembrar de bloquear em outro lugar. Também é muito rápido (como o MSDN diz, em CPUs modernas, isso geralmente é literalmente uma única instrução de CPU).
No entanto, não tenho muita certeza se ele contorna outras CPUs, reordenando as coisas ou se você também precisa combinar volátil com o incremento.InterlockedNotes:
Nota de rodapé: Para que serve realmente o volátil.
Como
volatile
não impede esses tipos de problemas de multithreading, para que serve? Um bom exemplo é dizer que você tem dois threads, um que sempre grava em uma variável (por exemploqueueLength
) e outro que sempre lê a mesma variável.Se
queueLength
não for volátil, o segmento A pode gravar cinco vezes, mas o segmento B pode ver essas gravações como atrasadas (ou mesmo potencialmente na ordem errada).Uma solução seria bloquear, mas você também pode usar volátil nessa situação. Isso garantiria que o segmento B sempre visse a coisa mais atualizada que o segmento A escreveu. Observe, no entanto, que essa lógica só funciona se você tiver escritores que nunca leem e leitores que nunca escrevem, e se o que você está escrevendo é um valor atômico. Assim que você fizer uma única leitura, modificação e gravação, precisará acessar as operações de bloqueio ou usar um bloqueio.
fonte
EDIT: Como observado nos comentários, hoje em dia fico feliz em usar
Interlocked
para os casos de uma única variável em que obviamente está tudo bem. Quando ficar mais complicado, ainda voltarei a travar ...O uso
volatile
não ajudará quando você precisar incrementar - porque a leitura e a gravação são instruções separadas. Outro encadeamento pode alterar o valor depois de ler, mas antes de escrever novamente.Pessoalmente, quase sempre travo - é mais fácil acertar de uma maneira obviamente correta do que a volatilidade ou o Interlocked.Increment. Para mim, o multi-threading sem bloqueio é para especialistas em threading, dos quais eu não sou um. Se Joe Duffy e sua equipe construírem boas bibliotecas que paralelizarão as coisas sem o bloqueio de algo que eu construiria, isso é fabuloso, e vou usá-lo em um piscar de olhos - mas, quando estou fazendo a rosquinha, tento mantenha simples.
fonte
"
volatile
" não substituiInterlocked.Increment
! Apenas garante que a variável não seja armazenada em cache, mas usada diretamente.Incrementar uma variável requer, na verdade, três operações:
Interlocked.Increment
executa as três partes como uma única operação atômica.fonte
volatile
se não se certificar de que a variável não está em cache. Ele apenas coloca restrições sobre como ele pode ser armazenado em cache. Por exemplo, ele ainda pode ser armazenado em cache em itens do cache L2 da CPU, porque eles são tornados coerentes no hardware. Ainda pode ser escolhido. As gravações ainda podem ser postadas no cache e assim por diante. (O que eu acho que era o que Zach queria chegar.)Bloqueio ou incremento intertravado é o que você está procurando.
Definitivamente, o volátil não é o que você procura - ele simplesmente instrui o compilador a tratar a variável como sempre mudando, mesmo que o caminho do código atual permita ao compilador otimizar uma leitura da memória caso contrário.
por exemplo
se m_Var estiver definido como false em outro encadeamento, mas não for declarado como volátil, o compilador estará livre para torná-lo um loop infinito (mas não significa que sempre o fará), verificando se há um registro de CPU (por exemplo, EAX porque o que o m_Var foi buscado desde o início) em vez de emitir outra leitura para o local da memória do m_Var (isso pode ser armazenado em cache - não sabemos e não nos importamos, e esse é o ponto de coerência do cache de x86 / x64). Todas as postagens anteriores de outras pessoas que mencionaram a reordenação de instruções simplesmente mostram que não entendem arquiteturas x86 / x64. Volátil nãoemita barreiras de leitura / gravação, conforme implícito nas postagens anteriores, dizendo 'impede a reordenação'. De fato, graças ao protocolo MESI, garantimos que o resultado que lemos é sempre o mesmo entre as CPUs, independentemente de os resultados reais terem sido retirados para a memória física ou simplesmente resididos no cache da CPU local. Não vou muito longe nos detalhes disso, mas tenha certeza de que, se isso der errado, a Intel / AMD provavelmente emitirá um recall do processador! Isso também significa que não precisamos nos preocupar com a execução fora de ordem, etc. Os resultados sempre garantem a retirada em ordem - caso contrário, estamos cheios!
Com o Incremento intertravado, o processador precisa sair, buscar o valor no endereço fornecido, incrementá-lo e gravá-lo novamente - tudo isso enquanto detém a propriedade exclusiva de toda a linha de cache (lock xadd) para garantir que nenhum outro processador possa modificar seu valor.
Com o volátil, você ainda terá apenas 1 instrução (assumindo que o JIT seja eficiente como deveria) - inc dword ptr [m_Var]. No entanto, o processador (cpuA) não solicita a propriedade exclusiva da linha de cache, enquanto faz tudo o que fez com a versão intertravada. Como você pode imaginar, isso significa que outros processadores podem escrever um valor atualizado de volta para m_Var após ser lido pela cpuA. Então, em vez de agora aumentar o valor duas vezes, você acaba com apenas uma vez.
Espero que isso esclareça o problema.
Para obter mais informações, consulte 'Compreender o impacto das técnicas de bloqueio baixo em aplicativos multithread' - http://msdn.microsoft.com/en-au/magazine/cc163715.aspx
ps O que levou essa resposta muito tardia? Todas as respostas foram flagrantemente incorretas (especialmente a marcada como resposta) em suas explicações. Eu apenas tive que esclarecer isso para qualquer pessoa que estivesse lendo isso. encolher de ombros
pps Estou assumindo que o destino é x86 / x64 e não IA64 (ele tem um modelo de memória diferente). Observe que as especificações de ECMA da Microsoft estão erradas, pois especifica o modelo de memória mais fraco em vez do mais forte (é sempre melhor especificar contra o modelo de memória mais forte para que seja consistente entre as plataformas - caso contrário, o código seria executado 24-7 no x86 / x64 pode não ser executado no IA64, embora a Intel tenha implementado um modelo de memória igualmente forte para o IA64) - A Microsoft admitiu isso por conta própria - http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx .
fonte
As funções intertravadas não travam. Eles são atômicos, o que significa que podem ser concluídos sem a possibilidade de uma alternância de contexto durante o incremento. Portanto, não há chance de conflito ou espera.
Eu diria que você sempre deve preferir a um bloqueio e incremento.
O volátil é útil se você precisar que as gravações em um encadeamento sejam lidas em outro e se desejar que o otimizador não reordene as operações em uma variável (porque coisas estão acontecendo em outro encadeamento que o otimizador não conhece). É uma escolha ortogonal de como você incrementa.
Este é um artigo muito bom, se você quiser ler mais sobre código sem bloqueio e a maneira correta de abordá-lo.
http://www.ddj.com/hpc-high-performance-computing/210604448
fonte
O bloqueio (...) funciona, mas pode bloquear um encadeamento e pode causar um conflito se outro código estiver usando os mesmos bloqueios de maneira incompatível.
Interlocked. * É a maneira correta de fazer isso ... muito menos sobrecarga, pois as CPUs modernas suportam isso como uma primitiva.
volátil por si só não está correto. Um encadeamento que tenta recuperar e depois gravar um valor modificado ainda pode entrar em conflito com outro encadeamento que faz o mesmo.
fonte
Fiz alguns testes para ver como a teoria realmente funciona: kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html . Meu teste foi mais focado no CompareExchnage, mas o resultado para o Increment é semelhante. O intertravamento não é necessário mais rapidamente no ambiente com várias CPUs. Aqui está o resultado do teste para o Increment em um servidor com 16 anos e 2 anos de CPU. Lembre-se de que o teste também envolve a leitura segura após o aumento, o que é típico no mundo real.
fonte
Concordo com a resposta de Jon Skeet e quero adicionar os seguintes links para todos que desejam saber mais sobre "volátil" e Intertravado:
Atomicidade, volatilidade e imutabilidade são diferentes, parte um - (Fabulous Adventures In Coding, de Eric Lippert)
Atomicidade, volatilidade e imutabilidade são diferentes, parte dois
Atomicidade, volatilidade e imutabilidade são diferentes, parte três
Sayonara Volatile - (instantâneo da Wayback Machine do blog de Joe Duffy, que apareceu em 2012)
fonte
Eu gostaria de adicionar à mencionado em outras respostas a diferença entre
volatile
,Interlocked
elock
:A palavra-chave volátil pode ser aplicada aos campos desses tipos :
sbyte
,byte
,short
,ushort
,int
,uint
,char
,float
, ebool
.byte
,sbyte
,short
, ushort,int
, ouuint
.IntPtr
eUIntPtr
.Outros tipos , incluindo
double
elong
, não podem ser marcados como "voláteis" porque não é possível garantir que as leituras e gravações nos campos desses tipos sejam atômicas. Para proteger o acesso multiencadeado a esses tipos de campos, use osInterlocked
membros da classe ou proteja o acesso usando alock
instruçãofonte