Pergunta

Quando escrevo um valor em um campo, que garante que eu recebo sobre quando o novo valor será salvo na memória principal? Por exemplo, como sei que o processador não mantém o novo valor em seu cache privado, mas atualizou a memória principal?
Outro exemplo:

int m_foo;

void Read() // executed by thread X (on processor #0)
{
   Console.Write(m_foo);
}

void Write() // executed by thread Y (on processor #1)
{
   m_foo = 1;
}

Existe a possibilidade de que depois Escreva() terminou de executar, algum outro thread executado Ler() Mas na verdade verá "0" como o valor atual? (Como talvez a gravação anterior para M_foo ainda não esteve?).
Que tipo de primitivas (ao lado de bloqueios) estão disponíveis para garantir que as gravações fossem lavadas?


EDITAR
Na amostra de código que usei, a gravação e a leitura são colocadas em métodos diferentes. Thread.MemoryBarrier não afeta apenas a reordenação de instruções que existem no mesmo escopo?

Além disso, vamos supor que eles não serão inlinados pelo JIT, como posso garantir que o valor escrito para m_foo não seja armazenado em um registro, mas na memória principal? (ou quando o M_FOO for lido, ele não receberá um valor antigo do cache da CPU).

É possível conseguir isso sem usar bloqueios ou a palavra -chave 'volátil'? (Além disso, digamos que não estou usando tipos primitivos, mas uma palavra de tamanho de palavra Então volátil não pode ser aplicado.)

Foi útil?

Solução

Volátil e interligado já foram mencionados, você pediu primitivas, uma adição à lista é usar Thread.MemoryBarrier() Antes de suas gravações ou leituras. Isso garante que nenhuma reordenação é feita de gravações e leituras de memória.

Isso está fazendo "à mão" o que lock, Interlocked e volatile pode fazer automaticamente a maioria das vezes. Você pode usar isso como um substituto completo para qualquer outra técnica, mas é sem dúvida o caminho mais difícil de viajar, e diz o MSDN:

"É difícil construir programas multithread corretos usando o MemoryBarrier. Para a maioria dos propósitos, a instrução C# Lock, a instrução Visual Basic Syncock e os métodos da classe Monitor fornecem maneiras mais fáceis e menos propensas a sincronizar acessos de memória. Recomendamos que você os usa em vez de MemoryBrerier. "

Como usar o MemoryBarrier

Um exemplo muito bom são as implementações de VolatileRead e VolatileWrite, que ambos usam internamente MemoryBarrier. A regra básica a seguir é: Quando você ler uma variável, coloque uma barreira de memória depois a leitura. Quando você escreve o valor, a barreira da memória deve vir antes da a escrita.

Caso você duvide se isso é menos eficiente então lock, considere que o bloqueio nada mais é do que "esgrima completa", na medida em que coloca uma barreira de memória antes e depois do bloco de código (ignorando o monitor por um momento). Este princípio é bem explicado neste Excelente artigo definitivo sobre fios, bloqueio, barreiras voláteis e de memória de Albahari.

Do refletor:

public static void VolatileWrite(ref byte address, byte value)
{
    MemoryBarrier();
    address = value;
}

public static byte VolatileRead(ref byte address)
{
    byte num = address;
    MemoryBarrier();
    return num;
}

Outras dicas

Se você deseja garantir que seja escrito prontamente e em ordem, marque-o como volatile, ou (com mais dor) uso Thread.VolatileRead / Thread.VolatileWrite (Não é uma opção atraente e fácil de perder uma, tornando -a inútil).

volatile int m_foo;

Caso contrário, você praticamente não tem garantias de nada (assim que fala vários threads).

Você também pode querer ver bloqueio (Monitor) ou Interlocked, que alcançaria o mesmo efeito enquanto a mesmo A abordagem é usada de todo o acesso (ou seja, todos lock, ou tudo Interlocked, etc).

Desde que você não use nenhuma sincronização, você não tem garantia de que um thread em execução em um processador veja as alterações feitas por outro thread em execução em outro processador. Isso ocorre porque o valor pode ser armazenado em cache nos caches da CPU ou em um registro da CPU.

Portanto, você precisa marcar a variável como volátil. Isso criará uma realização 'acontecer antes' entre ler as gravações.

Esse não é um problema de cache do processador. As gravações geralmente são aprofundadas (as gravações vão para o cache e a memória principal) e todas as leituras acessarão o cache. Mas existem muitos outros caches a caminho (linguagem de programação, bibliotecas, sistema operacional, E/S. buffers, etc.). O compilador também pode optar por manter uma variável em um registro do processador e nunca escrevê -lo na memória principal (é para isso que o operador volátil foi projetado, evite armazenar valor no registro quando pode ser uma E/S mapeada de memória).

Se você possui vários processos ou vários threads e a sincronização é um problema que você deve fazê -lo explicitamente, há muitas maneiras de fazê -lo, dependendo do caso de uso.

Para um único programa rosqueado, não se importe, o compilador fará o que deve e as leituras acessarão o que foi escrito.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top