Como sistemas de rosca lidar com dados compartilhados estar sendo armazenada em cache por cpus diferente?

StackOverflow https://stackoverflow.com/questions/1105502

Pergunta

Eu estou vindo em grande parte a partir de uma c ++ fundo, mas acho que esta questão se aplica a enfiar em qualquer idioma. Aqui está o cenário:

  1. Temos dois threads (ThreadA e ThreadB) e um valor x em memória compartilhada

  2. Assume-se que o acesso aos x é adequadamente controlada por uma extensão mútua (ou outros tipos de controlo de sincronização apropriado)

  3. Se os fios acontecer para rodar em processadores diferentes, o que acontece se ThreadA executa uma operação de gravação, mas seu processador coloca o resultado em seu cache L2 em vez da memória principal? Então, se ThreadB tenta ler o valor, será que não basta olhar em seu próprio cache L1 / L2 / memória principal e, em seguida, trabalho com qualquer valor antigo estava lá?

Se isso não for o caso, então como é que esta questão gerenciado?

Se for esse o caso, então o que pode ser feito sobre isso?

Foi útil?

Solução

Seu exemplo poderia funcionar muito bem.

Vários processadores usar um href="http://en.wikipedia.org/wiki/Coherence_protocol" rel="noreferrer"> protocolo de coerência como MESI para garantir que os restos de dados em sincronia entre as caches. Com MESI, cada linha de cache é considerado ser ou modificado, detido exclusivamente, compartilhada entre CPU de, ou inválido. Escrever uma linha de cache que é compartilhado entre as forças processadores ele se torne inválida no outro CPU de, mantendo as caches em sincronia.

No entanto, isso não é o bastante. Diferentes processadores têm diferentes modelos de memória , ea maioria dos processadores modernos suportam algum nível de memória re-ordenação acessos. Nestes casos, barreiras de memória são necessários.

Por exemplo, se você tem Thread A:

DoWork();
workDone = true;

E Linha B:

while (!workDone) {}
DoSomethingWithResults()

Com ambos rodando em processadores separados, não há nenhuma garantia de que as gravações feitas dentro DoWork () será visível para enfiar B antes da gravação para workDone e DoSomethingWithResults () iria proceder com o estado potencialmente inconsistente. barreiras de memória garantir alguma ordenação do lê e escreve - adicionando uma barreira de memória depois de DoWork () in Thread A forçaria todos lê / escreve feito por DoWork para completar antes da gravação para workDone, então esse segmento B teria uma visão consistente. Mutexes inerentemente fornecer uma barreira de memória, de modo que lê / escreve não pode passar uma chamada para bloqueio e desbloqueio.

No seu caso, um processador seria um sinal para os outros que sujou uma linha de cache e forçar os outros processadores de recarga da memória. Adquirir o mutex a ler e escrever as garantias de valor que a mudança para a memória é visível para o outro processador na ordem esperada.

Outras dicas

barreiras

A maioria das primitivas de bloqueio como mutexes implicam memória . Estes vigor um flush cache e recarregar a ocorrer.

Por exemplo,

ThreadA {
    x = 5;         // probably writes to cache
    unlock mutex;  // forcibly writes local CPU cache to global memory
}
ThreadB {
    lock mutex;    // discards data in local cache
    y = x;         // x must read from global memory
}

Em geral, o compilador entende memória compartilhada, e leva um esforço considerável para assegurar que a memória compartilhada é colocado em um lugar compartilhável. compiladores modernos são muito complicado da maneira que eles ordem operações e acessos de memória; eles tendem a compreender a natureza do threading e memória compartilhada. Isso não quer dizer que eles são perfeitos, mas, em geral, grande parte da preocupação é tomado cuidado de pelo compilador.

C # tem algum construção em apoio a este tipo de problemas. Você pode marcar uma variável com a palavra-chave volatile, o que obriga a ser sincronizados em todos os cpu do.

public static volatile int loggedUsers;

A outra parte é uma wrappper sintáctica em torno dos métodos NET chamados Threading.Monitor.Enter (x) e Threading.Monitor.Exit (x), onde x é uma variável de bloqueio. Isso faz com que outros segmentos tentando bloqueio X ter que esperar até o fio de bloqueio chama Exit (x).

public list users;
// In some function:
System.Threading.Monitor.Enter(users);
try {
   // do something with users
}
finally {
   System.Threading.Monitor.Exit(users);
}
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top