Volátil vs. Intertravado vs. Bloqueio

671

Digamos que uma classe tenha um public int countercampo 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 counterpara public volatile.

Agora que descobri volatile, removi muitas lockinstruções e o uso de Interlocked. Mas há uma razão para não fazer isso?

testemunho
fonte
Leia a referência Threading em C # . Abrange os meandros da sua pergunta. Cada um dos três tem finalidades diferentes e efeitos colaterais.
spoulson 30/09/08
1
simple-talk.com/blogs/2012/01/24/… você pode ver o uso de volitable em matrizes, não o entendo completamente, mas é outra referência ao que isso faz.
eran otzap
50
É como dizer "Descobri que o sistema de aspersão nunca é ativado, então vou removê-lo e substituí-lo por alarmes de fumaça". A razão para não fazer isso é porque é incrivelmente perigoso e quase não oferece benefícios . Se você tiver tempo para mudar o código, encontre uma maneira de torná-lo menos multithread ! Não encontre uma maneira de tornar o código multithread mais perigoso e fácil de quebrar!
Eric Lippert
1
Minha casa tem sprinklers e alarmes de fumaça. Ao incrementar um contador em um segmento e lê-lo em outro, parece que você precisa de um bloqueio (ou um bloqueio) e a palavra-chave volátil. Verdade?
yoyo
2
@ yoyo Não, você não precisa dos dois.
David Schwartz

Respostas:

859

Pior (na verdade não vai funcionar)

Altere o modificador de acesso de counterparapublic volatile

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:

lock(this.locker) this.counter++;

É seguro fazer isso (desde que você lembre-se de lockqualquer outro lugar que acessar this.counter). Impede que outros threads executem qualquer outro código que é protegido por locker. 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 lockerem outro lugar que não esteja realmente relacionado, poderá acabar bloqueando os outros threads sem motivo.

Melhor

Interlocked.Increment(ref this.counter);

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:

  1. MÉTODOS INTERLOCADOS SÃO CONCORRENTEMENTE SEGUROS EM QUALQUER NÚMERO DE CORES OU CPUs.
  2. Os métodos intertravados aplicam uma barreira completa em torno das instruções que eles executam, para que a reordenação não ocorra.
  3. Os métodos intertravados não precisam ou nem oferecem suporte ao acesso a um campo volátil , pois o volátil é colocado meia cerca em torno das operações em um determinado campo e o intertravado está usando a cerca completa.

Nota de rodapé: Para que serve realmente o volátil.

Como volatilenã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 exemplo queueLength) e outro que sempre lê a mesma variável.

Se queueLengthnã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 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.

Orion Edwards
fonte
29
"Não tenho muita certeza ... se você também precisa combinar volátil com o incremento." Eles não podem ser combinados AFAIK, pois não podemos passar um volátil por ref. Ótima resposta, a propósito.
Hosam Aly 17/01/09
41
Muito obrigado! Sua nota de rodapé sobre "O que é realmente bom para o volátil" é o que eu estava procurando e confirmou como quero usá-lo.
Jacques Bosch
7
Em outras palavras, se um var for declarado como volátil, o compilador assumirá que o valor do var não permanecerá o mesmo (ou seja, volátil) toda vez que seu código o encontrar. Portanto, em um loop como: while (m_Var) {} e m_Var é definido como false em outro thread, o compilador não verifica simplesmente o que já está em um registro que foi carregado anteriormente com o valor de m_Var, mas lê o valor em m_Var novamente. No entanto, isso não significa que não declarar volátil fará com que o loop continue infinitamente - especificar volátil apenas garante que isso não acontecerá se m_Var estiver definido como false em outro encadeamento.
Zach Saw
35
@Zach Saw: Sob o modelo de memória para C ++, volátil é como você o descreveu (basicamente útil para memória mapeada por dispositivo e não muito mais). Sob o modelo de memória para o CLR (essa pergunta está marcada como C #), é que o volátil inserirá barreiras de memória em torno de leituras e gravações nesse local de armazenamento. Barreiras de memória (e variações especiais bloqueado de algumas instruções de montagem) é você que você diga o processador não coisas reordenar e eles são bastante importante ...
Orion Edwards
19
@ZachSaw: Um campo volátil em C # impede que o compilador C # e o compilador jit façam certas otimizações que armazenariam o valor em cache. Também garante certas garantias sobre a ordem em que as leituras e gravações podem ser observadas em vários threads. Como detalhe de implementação, isso pode ser feito através da introdução de barreiras de memória nas leituras e gravações. A semântica precisa garantida é descrita na especificação; observe que a especificação não garante que uma ordem consistente de todas as gravações e leituras voláteis seja observada por todos os threads.
Eric Lippert
147

EDIT: Como observado nos comentários, hoje em dia fico feliz em usar Interlockedpara os casos de uma única variável em que obviamente está tudo bem. Quando ficar mais complicado, ainda voltarei a travar ...

O uso volatilenã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.

Jon Skeet
fonte
16
+1 por garantir que eu esqueça a codificação sem bloqueio a partir de agora.
Xaqron
5
os códigos sem bloqueio definitivamente não são verdadeiramente livres de bloqueio, pois bloqueiam em algum momento - seja no nível do barramento (FSB) ou na interCPU, ainda há uma penalidade que você teria que pagar. No entanto, o bloqueio nesses níveis mais baixos geralmente é mais rápido, desde que você não sature a largura de banda de onde o bloqueio ocorre.
Zach Saw
2
Não há nada de errado com Interlocked, é exatamente o que você está procurando e mais rápido do que um fechamento completo ()
Jaap
5
@Jaap: Sim, esses dias eu iria usar intertravado para uma genuína contra única. Eu simplesmente não gostaria de começar a mexer na tentativa de descobrir interações entre várias atualizações sem bloqueio de variáveis.
Jon Skeet
6
@ZachSaw: Seu segundo comentário diz que as operações intertravadas "bloqueiam" em algum momento; o termo "bloqueio" geralmente implica que uma tarefa pode manter controle exclusivo de um recurso por um período ilimitado; a principal vantagem da programação sem bloqueio é que ela evita o perigo de o recurso se tornar inutilizável, como resultado da tarefa de possuir a solução. A sincronização de barramento usada pela classe intertravada não é apenas "geralmente mais rápida" - na maioria dos sistemas, ela tem um horário limitado na pior das hipóteses, enquanto as travas não.
Supercat 21/08
44

" volatile" não substitui Interlocked.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:

  1. ler
  2. incremento
  3. Escreva

Interlocked.Increment executa as três partes como uma única operação atômica.

Michael Damatov
fonte
4
Dito de outra maneira, as alterações intertravadas são totalmente vedadas e, como tal, são atômicas. Os membros voláteis são apenas parcialmente vedados e, como tal, não garantem a segurança de threads.
precisa saber é o seguinte
1
Na verdade, volatilese 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.)
David Schwartz
42

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

while (m_Var)
{ }

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 .

Zach Saw
fonte
3
Interessante. Você pode fazer referência a isso? Eu votaria feliz nisso, mas postar com uma linguagem agressiva três anos depois de uma resposta altamente votada que seja consistente com os recursos que li exigirá uma prova um pouco mais tangível.
Steven Evers
Se você pode apontar para qual parte deseja referenciar, ficaria feliz em desenterrar algumas coisas de algum lugar (duvido que tenha revelado segredos comerciais de fornecedores x86 / x64, portanto estes devem estar facilmente disponíveis no wiki, Intel PMR (manuais de referência do programador), blogs MSFT, MSDN ou algo similar) ...
Zach Saw
2
Por que alguém iria querer impedir o cache da CPU está além de mim. Todo o setor imobiliário (definitivamente não desprezível em tamanho e custo) dedicado à execução da coerência do cache é completamente desperdiçado, se esse for o caso ... A menos que você não exija coerência de cache, como uma placa gráfica, dispositivo PCI etc., você não definiria uma linha de cache para gravação.
perfil completo de Zach Saw
4
Sim, tudo o que você diz é se não 100%, pelo menos 99% na marca. Este site é (na maioria das vezes) bastante útil quando você está com pressa de desenvolver o trabalho, mas infelizmente a precisão das respostas correspondentes aos votos (do jogo) não existe. Portanto, basicamente no stackoverflow, você pode ter uma idéia do que é o entendimento popular dos leitores e não do que realmente é. Às vezes, as principais respostas são apenas palavrões puros - mitos do tipo. E, infelizmente, é isso que gera as pessoas que se deparam com a leitura enquanto resolvem o problema. É compreensível, porém, ninguém pode saber tudo.
user1416420
1
@BenVoigt Eu poderia continuar e responder sobre todas as arquiteturas em que o .NET roda, mas isso levaria algumas páginas e definitivamente não é adequado para SO. É muito melhor educar as pessoas com base no modelo de memória subjacente do .NET mais amplamente usado do que aquele que é arbitrário. E com meus comentários 'em todos os lugares', eu estava corrigindo os erros que as pessoas estavam cometendo ao assumir a liberação / invalidação do cache etc. Eles fizeram suposições sobre o hardware subjacente sem especificar qual hardware.
Zach Saw
16

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

Lou Franco
fonte
11

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.

Rob Walker
fonte
8

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.

D:\>InterlockVsMonitor.exe 16
Using 16 threads:
          InterlockAtomic.RunIncrement         (ns):   8355 Average,   8302 Minimal,   8409 Maxmial
    MonitorVolatileAtomic.RunIncrement         (ns):   7077 Average,   6843 Minimal,   7243 Maxmial

D:\>InterlockVsMonitor.exe 4
Using 4 threads:
          InterlockAtomic.RunIncrement         (ns):   4319 Average,   4319 Minimal,   4321 Maxmial
    MonitorVolatileAtomic.RunIncrement         (ns):    933 Average,    802 Minimal,   1018 Maxmial
Kenneth Xu
fonte
O exemplo de código que você testou foi muuuuito trivial - não faz muito sentido testá-lo dessa maneira! O melhor seria entender o que os diferentes métodos estão realmente fazendo e usar o apropriado com base no cenário de uso que você possui.
Zach Saw
@Zach, como foi discutido aqui o cenário de aumentar um contador de maneira segura para threads. Que outro cenário de uso estava em sua mente ou como você o testaria? Obrigado pelo comentário BTW.
Kenneth Xu
O ponto é que é um teste artificial. Você não vai martelar o mesmo local que frequentemente em qualquer cenário do mundo real. Se estiver, então você está com gargalo no FSB (como mostrado nas caixas do servidor). Enfim, olhe para a minha resposta no seu blog.
amigos estão dizendo sobre zach saw
2
Olhando de volta novamente. Se o verdadeiro gargalo estiver com o FSB, a implementação do monitor deve observar o mesmo gargalo. A diferença real é que a Interlocked está aguardando e tentando novamente, o que se torna um problema real com a contagem de alto desempenho. Pelo menos, espero que meu comentário chame a atenção de que o Interlocked nem sempre é a escolha certa para a contagem. O fato de as pessoas estarem procurando alternativas explicava bem. Você precisa de um longo víbora gee.cs.oswego.edu/dl/jsr166/dist/jsr166edocs/jsr166e/...
Kenneth Xu
8

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)

zihotki
fonte
3

Eu gostaria de adicionar à mencionado em outras respostas a diferença entre volatile, Interlockede lock:

A palavra-chave volátil pode ser aplicada aos campos desses tipos :

  • Tipos de referência.
  • Tipos de ponteiro (em um contexto não seguro). Observe que, embora o ponteiro em si possa ser volátil, o objeto para o qual ele aponta não pode. Em outras palavras, você não pode declarar um "ponteiro" como "volátil".
  • Tipos simples, como sbyte, byte, short, ushort, int, uint, char, float, e bool.
  • Um tipo de enumeração com um dos seguintes tipos de bases: byte, sbyte, short, ushort, int, ou uint.
  • Parâmetros de tipo genérico conhecidos por serem tipos de referência.
  • IntPtre UIntPtr.

Outros tipos , incluindo doublee long, 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 os Interlockedmembros da classe ou proteja o acesso usando a lockinstrução

Vadim S.
fonte