a atribuição de referência é atômica, então por que Interlocked.Exchange (ref Object, Object) é necessário?

108

Em meu serviço da web asmx multithread, eu tinha um campo de classe _allData do meu próprio tipo SystemData, que consiste em poucos List<T>e está Dictionary<T>marcado como volatile. Os dados do sistema ( _allData) são atualizados de vez em quando e eu faço isso criando outro objeto chamado newDatae preenchendo suas estruturas de dados com novos dados. Quando terminar, eu apenas atribuo

private static volatile SystemData _allData

public static bool LoadAllSystemData()
{
    SystemData newData = new SystemData();
    /* fill newData with up-to-date data*/
     ...
    _allData = newData.
} 

Isso deve funcionar, já que a atribuição é atômica e os threads que têm a referência aos dados antigos continuam a usá-los e o resto tem os novos dados do sistema logo após a atribuição. No entanto, meu colega disse que em vez de usar volatilepalavras-chave e InterLocked.Exchangeatribuição simples, eu deveria usar porque ele disse que em algumas plataformas não é garantido que a atribuição de referência seja atômica. Além disso: quando eu declaro the _allDatacampo como volatileo

Interlocked.Exchange<SystemData>(ref _allData, newData); 

produz um aviso "uma referência a um campo volátil não será tratada como volátil" O que devo pensar sobre isso?

char m
fonte

Respostas:

179

Existem inúmeras perguntas aqui. Considerando-os um de cada vez:

a atribuição de referência é atômica, então por que Interlocked.Exchange (ref Object, Object) é necessário?

A atribuição de referência é atômica. Interlocked.Exchange não faz apenas referência à atribuição. Ele faz uma leitura do valor atual de uma variável, armazena o valor antigo e atribui o novo valor à variável, tudo como uma operação atômica.

meu colega disse que em algumas plataformas não é garantido que a atribuição de referência seja atômica. Meu colega estava correto?

Não. A atribuição de referência tem garantia de ser atômica em todas as plataformas .NET.

Meu colega está raciocinando com base em premissas falsas. Isso significa que suas conclusões estão incorretas?

Não necessariamente. Seu colega pode estar lhe dando bons conselhos por motivos ruins. Talvez haja algum outro motivo pelo qual você deva usar o Interlocked.Exchange. Programar sem travas é extremamente difícil e no momento em que você se afasta das práticas bem estabelecidas adotadas por especialistas na área, você está perdido e se arriscando nas piores condições de corrida. Não sou um especialista neste campo nem um especialista em seu código, portanto, não posso fazer um julgamento de uma forma ou de outra.

produz um aviso "uma referência a um campo volátil não será tratada como volátil" O que devo pensar sobre isso?

Você deve entender porque isso é um problema em geral. Isso levará a uma compreensão de por que o aviso não é importante neste caso específico.

O motivo pelo qual o compilador dá este aviso é porque marcar um campo como volátil significa "este campo será atualizado em vários segmentos - não gere nenhum código que armazene em cache os valores deste campo e certifique-se de que qualquer leitura ou gravação de este campo não é "movido para frente e para trás no tempo" por meio de inconsistências do cache do processador. "

(Presumo que você já entenda tudo isso. Se você não tem uma compreensão detalhada do significado de volátil e como ele afeta a semântica do cache do processador, então você não entende como funciona e não deveria usar volátil. Programas sem bloqueio são muito difíceis de acertar; certifique-se de que seu programa está certo, porque você entende como ele funciona, não acertadamente por acidente.)

Agora suponha que você crie uma variável que é um apelido de um campo volátil, passando um ref para esse campo. Dentro do método chamado, o compilador não tem nenhuma razão para saber que a referência precisa ter semântica volátil! O compilador gerará alegremente o código para o método que falha em implementar as regras para campos voláteis, mas a variável é um campo volátil. Isso pode destruir completamente sua lógica sem bloqueio; a suposição é sempre que um campo volátil é sempre acessado com semântica volátil. Não faz sentido tratá-lo como volátil algumas vezes e não outras; você tem que ser sempre consistente, caso contrário não pode garantir consistência em outros acessos.

Portanto, o compilador avisa quando você faz isso, porque provavelmente vai bagunçar completamente sua lógica livre de bloqueio cuidadosamente desenvolvida.

Claro, Interlocked.Exchange foi escrito para esperar um campo volátil e fazer a coisa certa. O aviso é, portanto, enganoso. Lamento muito isso; o que deveríamos ter feito é implementar algum mecanismo pelo qual um autor de um método como Interlocked.Exchange poderia colocar um atributo no método dizendo "este método que recebe uma referência impõe semântica volátil na variável, então suprima o aviso". Talvez em uma versão futura do compilador façamos isso.

Eric Lippert
fonte
1
Pelo que ouvi, a Interlocked.Exchange também garante que uma barreira de memória seja criada. Portanto, se você, por exemplo, criar um novo objeto, atribuir algumas propriedades e, em seguida, armazenar o objeto em outra referência sem usar Interlocked.Exchange, o compilador pode bagunçar a ordem dessas operações, tornando assim o acesso à segunda referência, não ao thread seguro. É realmente assim? Faz sentido usar Interlocked.Exchange é esse tipo de cenário?
Mike
12
@ Mike: Quando se trata do que é possivelmente observado em situações multithread de low-lock, sou tão ignorante quanto qualquer pessoa. A resposta provavelmente variará de processador para processador. Você deve encaminhar sua pergunta a um especialista ou ler sobre o assunto se for do seu interesse. O livro e o blog de Joe Duffy são bons lugares para começar. Minha regra: não use multithreading. Se necessário, use estruturas de dados imutáveis. Se você não puder, use bloqueios. Somente quando você deve ter dados mutáveis ​​sem bloqueios, você deve considerar técnicas de baixo bloqueio.
Eric Lippert
Obrigado pela sua resposta Eric. De fato, me interessa, é por isso que tenho lido livros e blogs sobre estratégias de multithreading e travamento e também tento implementá-las em meu código. Mas ainda há muito a aprender ...
Mike
2
@EricLippert Entre "não use multithreading" e "se for necessário, use estruturas de dados imutáveis", eu inseriria o nível intermediário e muito comum de "ter um thread filho usando apenas objetos de entrada de propriedade exclusiva e o thread pai consumir os resultados só quando a criança terminar ". Como em var myresult = await Task.Factory.CreateNew(() => MyWork(exclusivelyLocalStuffOrValueTypeOrCopy));.
João
1
@John: Essa é uma boa ideia. Tento tratar os threads como processos baratos: eles estão lá para fazer um trabalho e produzir um resultado, não para serem um segundo thread de controle dentro das estruturas de dados do programa principal. Mas se a quantidade de trabalho que o thread está realizando é tão grande que é razoável tratá-lo como um processo, então eu digo apenas transforme-o em um processo!
Eric Lippert
9

Ou seu colega está errado ou sabe algo que a especificação da linguagem C # não sabe.

5.5 Atomicidade de referências variáveis :

"Leituras e gravações dos seguintes tipos de dados são atômicas: bool, char, byte, sbyte, short, ushort, uint, int, float e tipos de referência."

Portanto, você pode gravar na referência volátil sem o risco de obter um valor corrompido.

Obviamente, você deve ter cuidado ao decidir qual thread deve buscar os novos dados, para minimizar o risco de que mais de um thread por vez faça isso.

Guffa
fonte
3
@guffa: sim, também li isso. isso deixa a questão original "a atribuição de referência é atômica, então por que Interlocked.Exchange (ref Object, Object) é necessário?" sem resposta
char em
@zebrabox: o que você quer dizer? quando eles não são? o que você faria?
char em
@matti: é necessário quando você precisa ler e escrever um valor como uma operação atômica.
Guffa
Com que freqüência você realmente precisa se preocupar com a memória não estar alinhada corretamente no .NET? Coisas pesadas de interoperabilidade?
Skurmedel
1
@zebrabox: A especificação não lista essa ressalva, ela fornece uma declaração muito clara. Você tem uma referência para uma situação não alinhada à memória em que uma leitura ou gravação de referência não seja atômica? Parece que isso violaria a linguagem muito clara na especificação.
TJ Crowder
6

Interlocked.Exchange <T>

Define uma variável do tipo especificado T para um valor especificado e retorna o valor original, como uma operação atômica.

Muda e devolve o valor original, não adianta porque só quer mudar e, como disse o Guffa, já é atômico.

A menos que um criador de perfil comprove que é um gargalo em seu aplicativo, você deve considerar desbloquear bloqueios, é mais fácil entender e provar que seu código está certo.

Guillaume
fonte
3

Iterlocked.Exchange() não é apenas atômico, mas também cuida da visibilidade da memória:

As seguintes funções de sincronização usam as barreiras apropriadas para garantir o pedido de memória:

Funções que entram ou saem de seções críticas

Funções que sinalizam objetos de sincronização

Funções de espera

Funções interligadas

Problemas de sincronização e multiprocessador

Isso significa que, além da atomicidade, ele garante que:

  • Para o tópico que o chama:
    • Nenhuma reordenação das instruções é feita (pelo compilador, o tempo de execução ou o hardware).
  • Para todos os tópicos:
    • Nenhuma leitura na memória que aconteça antes desta instrução verá a alteração feita por ela.
    • Todas as leituras após esta instrução verão a alteração feita por esta instrução.
    • Todas as gravações na memória após essa instrução acontecerão depois que essa alteração de instrução atingir a memória principal (liberando essa alteração de instrução para a memória principal quando for concluída e não permitindo que o hardware a liberte em seu tempo de ativação).
selalerer
fonte