Como sistemas de rosca lidar com dados compartilhados estar sendo armazenada em cache por cpus diferente?
-
12-09-2019 - |
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:
-
Temos dois threads (ThreadA e ThreadB) e um valor x em memória compartilhada
-
Assume-se que o acesso aos x é adequadamente controlada por uma extensão mútua (ou outros tipos de controlo de sincronização apropriado)
-
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?
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
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);
}