Volátil vs. intertravado vs. bloqueio

Digamos que uma class tenha um campo public int counter acessado por vários threads. Este int só é incrementado ou decrementado.

Para incrementar este campo, qual abordagem deve ser usada e por quê?

  • lock(this.locker) this.counter++; ,
  • Interlocked.Increment(ref this.counter); ,
  • Altere o modificador de access do counter para public volatile .

Agora que descobri que é volatile , estou removendo muitas instruções de lock e o uso do Interlocked . Mas há uma razão para não fazer isso?

Pior (na verdade não funciona)

Alterar o modificador de access do counter para public volatile

Como outras pessoas mencionaram, isso por si só não é realmente seguro. O ponto de volatile é que vários threads em execução em várias CPUs podem armazenar dados em cache e reordenar as instruções.

Se não for volatile e a CPU A incrementar um valor, a CPU B pode não ver realmente 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 as leituras e escrever operações, que é o problema que você está tentando evitar.

Segundo melhor:

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

Isso é seguro fazer (desde que você lembre de lock qualquer outro lugar que você acessa esta this.counter ). Impede que qualquer outro encadeamento execute qualquer outro código que seja protegido pelo locker . Usando bloqueios também evita os problemas de reordenamento de múltiplas CPUs como acima, o que é ótimo.

O problema é que o bloqueio é lento, e se você reutilizar o locker em algum outro lugar que não esteja realmente relacionado, você pode acabar bloqueando seus outros tópicos sem nenhum motivo.

Melhor

Interlocked.Increment(ref this.counter);

Isso é seguro, pois efetivamente faz a leitura, o incremento e a gravação em ‘um hit’ que não pode ser interrompido. Por causa disso, isso não afetará nenhum outro código, e você não precisa se lembrar de bloquear outro lugar também. Também é muito rápido (como o MSDN diz, em CPUs modernas, isso geralmente é literalmente uma única instrução de CPU).

Eu não estou inteiramente certo, no entanto, se ele gira em torno de outras CPUs reordenando coisas, ou se você também precisa combinar o volátil com o incremento.

InterlockedNotes:

  1. MÉTODOS INTERLOCADOS SÃO CONCORRENTEMENTE SEGUROS EM QUALQUER NÚMERO DE CORE OU UCP.
  2. Os methods intertravados aplicam uma cerca completa em torno das instruções que eles executam, portanto, a reordenação não acontece.
  3. Os methods intertravados não precisam nem sequer suportam o access a um campo volátil , já que o volátil é colocado em uma cerca ao redor das operações em determinado campo e o intertravado usa a cerca completa.

Nota de rodapé: O que volátil é realmente bom.

Como o volatile não impede esses tipos de problemas de multithreading, para que serve? Um bom exemplo é dizer que você tem dois encadeamentos, um que sempre grava em uma variável (digamos queueLength ) e um que sempre lê dessa mesma variável.

Se queueLength não for volátil, o thread A poderá gravar cinco vezes, mas o thread B poderá ver essas gravações como atrasadas (ou até potencialmente na ordem errada).

Uma solução seria bloquear, mas você também pode usar o volátil nessa situação. Isso garantiria que o thread B sempre verá a coisa mais atualizada que o thread A escreveu. Note, entretanto, que essa lógica funciona se você tiver escritores que nunca leem, e leitores que nunca escrevem, e se a coisa que você está escrevendo é um valor atômico. Assim que você fizer uma única leitura-modificação-gravação, você precisará ir para Operações intertravadas ou usar um Bloqueio.

EDIT: Como observado nos comentários, estes dias eu estou feliz em usar Interlocked para os casos de uma única variável, onde é, obviamente, tudo bem. Quando fica mais complicado, ainda vou reverter para bloqueio …

Usar o volatile não ajudará quando você precisar incrementar – porque a leitura e a gravação são instruções separadas. Outro segmento pode alterar o valor depois de ler, mas antes de você escrever de volta.

Pessoalmente eu quase sempre apenas bloqueio – é mais fácil acertar de uma forma que é obviamente certo do que a volatilidade ou Interlocked.Increment. Tanto quanto eu estou preocupado, multi-threading livre de bloqueio é para especialistas de segmentação real, dos quais eu não sou um. Se Joe Duffy e sua equipe construírem bibliotecas legais que farão um paralelo com as coisas sem muito bloqueio como algo que eu construiria, isso é fabuloso, e eu vou usá-lo em um piscar de olhos – mas quando estou fazendo o threading eu tento mantenha simples.

volatile ” não substitui o 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 todas as três partes como uma única operação atômica.

Bloqueio ou incremento intertravado é o que você está procurando.

Volatile definitivamente não é o que você procura – ele simplesmente diz ao compilador para tratar a variável como sempre mudando mesmo que o caminho do código atual permita ao compilador otimizar uma leitura da memory de outra forma.

por exemplo

 while (m_Var) { } 

se m_Var é definido como false em outro thread, mas não é declarado como volátil, o compilador é livre para torná-lo um loop infinito (mas não significa que sempre será) fazendo com que ele seja verificado contra um registrador de CPU (por exemplo, EAX porque em que m_Var foi buscado desde o início) em vez de emitir outra leitura para o local de memory de 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). Todos os posts anteriores de outros que mencionaram a reordenação de instruções simplesmente mostram que eles não entendem as arquiteturas x86 / x64. O Volatile não emite barreiras de leitura / escrita, como sugerido pelos posts anteriores, dizendo que “impede a reordenação”. De fato, graças novamente ao protocolo MESI, garantimos que o resultado que lemos seja sempre o mesmo entre os processadores, independentemente de os resultados reais terem sido retirados para a memory física ou simplesmente residirem 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 emitiria 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 são garantidos para se aposentar em ordem – caso contrário, estamos cheios!

Com Incrementos interbloqueados, o processador precisa sair, buscar o valor do endereço fornecido, depois incrementá-lo e gravá-lo de volta – tudo isso enquanto possui a propriedade exclusiva de toda a linha de cache (bloquear xadd) para garantir que nenhum outro processador possa modificar seu valor.

Com volatile, você ainda vai acabar com apenas 1 instrução (assumindo que o JIT é 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 poderiam gravar um valor atualizado de volta ao m_Var após ele ter sido lido pelo cpuA. Então, ao invés de agora ter incrementado o valor duas vezes, você acaba com apenas uma vez.

Espero que isso limpe a questão.

Para obter mais informações, consulte ‘Compreender o impacto das técnicas de baixo bloqueio em aplicativos multithread’ – http://msdn.microsoft.com/pt-br/magazine/cc163715.aspx

ps O que motivou essa resposta tardia? Todas as respostas foram tão flagrantemente incorretas (especialmente aquela marcada como resposta) em sua explicação que eu só tinha que esclarecer para qualquer outra pessoa lendo isso. encolhe os ombros

pps Estou assumindo que o alvo é x86 / x64 e não IA64 (ele tem um modelo de memory diferente). Observe que as especificações de ECMA da Microsoft estão erradas, pois especifica o modelo de memory mais fraco em vez do mais forte (é sempre melhor especificar o modelo de memory mais forte para que seja consistente em todas as plataformas – caso contrário, seria executado 24-7 em x86 / O x64 pode não rodar no IA64, embora a Intel tenha implementado um modelo similar de memory para IA64) – a Microsoft admitiu isso – http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx .

Funções intertravadas não bloqueiam. Eles são atômicos, o que significa que eles podem ser concluídos sem a possibilidade de uma alternância de contexto durante o incremento. Portanto, não há chance de impasse ou espera.

Eu diria que você deve sempre preferir um bloqueio e incremento.

Volatile é útil se você precisa de gravações em um thread para ser lido em outro, e se você quiser que o otimizador não reordene as operações em uma variável (porque as coisas estão acontecendo em outro thread que o otimizador não conhece). É uma escolha ortogonal de como você incrementa.

Este é um artigo muito bom, se você quiser ler mais sobre o código livre de bloqueio, e o caminho certo para se aproximar de escrevê-lo

http://www.ddj.com/hpc-high-performance-computing/210604448

lock (…) funciona, mas pode bloquear um thread e pode causar deadlock se outro código estiver usando os mesmos bloqueios de maneira incompatível.

Intertravado. * É a maneira correta de fazer isso … muito menos sobrecarga, já que os processadores modernos suportam isso como um primitivo.

volátil por si só não está correto. Um thread tentando recuperar e, em seguida, escrever de volta um valor modificado ainda pode entrar em conflito com outro thread fazendo o mesmo.

Eu sigo a resposta de Jon Skeet e quero adicionar os seguintes links para todos que querem saber mais sobre “volátil” e Interlocked:

Atomicidade, volatilidade e imutabilidade são diferentes, primeira parte – (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 – (visão de Wayback Machine de Joe Duffy’s Weblog como apareceu em 2012)

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 Increment é semelhante. Intertravado não é necessário mais rápido no ambiente multi-cpu. Aqui está o resultado do teste para Incremento em um servidor de 2 anos de 16 CPUs. 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 

Leia o Threading em referência C # . Abrange os meandros da sua pergunta. Cada um dos três tem diferentes propósitos e efeitos colaterais.