Digamos que exista uma classe que contenha um campo contador público de int que pode ser acessado por múltiplos threads, e esse número só vai aumentar ou diminuir.
Ao adicionar esse campo, qual dos seguintes esquemas deve ser usado e por quê?
- lock(this.locker) this.counter++;
- Interlocked.Increment(ref this.counter);
- Mude o modificador de acesso de contador para volátil público
Pior (nenhum deles realmente funciona)
Mude o modificador de acesso de contador para volátil público
Essa abordagem na verdade não é segura, e o ponto sobre o volátil é que múltiplas threads rodando em múltiplas CPUs armazenam dados em buffer e reorganizam instruções executadas.
Se for não volátil, quando a CPU A aumenta em um valor, a CPU B precisa esperar um pouco para ver o valor aumentado, o que pode causar problemas.
Se for volátil, garante que ambos os CPUs vejam o mesmo valor ao mesmo tempo. Mas isso não evita operações de leitura e escrita que se cruzam.
Adicionar valor a uma variável na verdade leva três etapas
1. Leitura, 2. Adicionar 3. Escrever
Suponha que a thread A leia o valor do contador como 1 e não esteja pronta para aumentar, então a thread B também leia o valor do contador como 1, e então ambas as threads começam a realizar operações incrementais e de escrita. O valor do marcador final é 2. Isso não está certo, ambas as threads fizeram uma operação de aumento, e o resultado correto deveria ser 3. Portanto, rotulá-lo como volátil é simplesmente inseguro.
É melhor
lock(this.locker) this.counter++;
Assim fica seguro (lembre-se de trancar todos os lugares que quiser acessar esse balcão, claro). Isso impede que qualquer outra thread execute o código bloqueado. E também previne o problema de sequenciamento de instruções multi-CPU mencionado acima. O problema é que o lock tem desempenho lento, e se você usar o lock em outros lugares não relacionados, ele pode bloquear suas outras threads.
Melhores
Interlocked.Increment(ref this.counter);
Isso é seguro e muito eficiente. Ele realiza leitura, aumento, gravação de três operações em um átomo sem ser interrompido no meio. Como não afeta outros códigos, você não precisa lembrar das fechaduras em outros lugares. E também é muito rápido (como diz o MSDN, nos processadores de hoje, geralmente é só uma instrução).
Mas não tenho certeza se isso também resolve o problema de ordenação de instruções da CPU, ou se precisa ser usado em conjunto com o volátil e esse incremento.
Suplemento: Quais problemas voláteis resolve bem?
Como o volátil não pode impedir o multithreading, o que ele pode fazer? Um bom exemplo é que você tem dois threads, um sempre escreve para uma variável, digamos que essa variável seja queneLength, e o outro sempre lê dados dessa variável. Se o comprimento da fila não for volátil, a thread A pode ser lida 5 vezes, mas a thread B pode apresentar dados atrasados, ou até mesmo dados na ordem errada. Uma solução é usar o bloqueio, mas nesse caso você também pode usar volátil. Isso garante que a thread B sempre veja os dados mais recentes escritos pela thread A, mas essa lógica só funciona se você não ler quando escreve, e se não escrever na hora de ler. Quando múltiplas threads querem fazer operações de leitura-modificação-escrita, você precisa usar Interlock ou lock.
|