Ouvi e li vários artigos, palestras e perguntas sobre o stackoverflow std::atomic
e gostaria de ter certeza de que entendi bem. Como ainda estou um pouco confuso com a visibilidade das gravações de linha de cache devido a possíveis atrasos nos protocolos de coerência de cache MESI (ou derivados), buffers de armazenamento, filas de invalidação e assim por diante.
Eu li que o x86 tem um modelo de memória mais forte e que, se uma invalidação do cache for atrasada, o x86 poderá reverter as operações iniciadas. Mas agora estou interessado apenas no que devo assumir como programador de C ++, independentemente da plataforma.
[T1: thread1 T2: thread2 V1: variável atômica compartilhada]
Eu entendo que std :: atomic garante que,
(1) Nenhuma corrida de dados ocorre em uma variável (graças ao acesso exclusivo à linha de cache).
(2) Dependendo de qual ordem de memória usamos, garante (com barreiras) que a consistência sequencial acontece (antes de uma barreira, depois de uma barreira ou de ambas).
(3) Após uma gravação atômica (V1) em T1, um RMW atômico (V1) em T2 será coerente (sua linha de cache será atualizada com o valor gravado em T1).
Mas, como o primer de coerência do cache menciona,
A implicação de todas essas coisas é que, por padrão, as cargas podem buscar dados obsoletos (se uma solicitação de invalidação correspondente estava na fila de invalidação)
Então, o seguinte está correto?
(4) std::atomic
NÃO garante que T2 não leia um valor "obsoleto" em uma leitura atômica (V) após uma gravação atômica (V) em T1.
Perguntas se (4) está correto: se a gravação atômica em T1 invalida a linha de cache, independentemente do atraso, por que T2 está aguardando a invalidação ser eficaz quando uma operação RMW atômica, mas não em uma leitura atômica?
Perguntas se (4) está errado: quando um thread pode ler um valor "obsoleto" e "é visível" na execução, então?
Eu aprecio muito suas respostas
Atualização 1
Então parece que eu estava errado em (3) então. Imagine o seguinte intercalar, para um V1 inicial = 0:
T1: W(1)
T2: R(0) M(++) W(1)
Embora seja garantido que o RMW de T2 ocorra totalmente após W (1) nesse caso, ele ainda pode ler um valor "obsoleto" (eu estava errado). De acordo com isso, o atômico não garante a coerência total do cache, apenas a consistência sequencial.
Atualização 2
(5) Agora imagine este exemplo (x = y = 0 e é atômico):
T1: x = 1;
T2: y = 1;
T3: if (x==1 && y==0) print("msg");
de acordo com o que conversamos, ver o "msg" exibido na tela não nos forneceria informações além do que T2 foi executado após T1. Portanto, uma das seguintes execuções pode ter acontecido:
- T1 <T3 <T2
- T1 <T2 <T3 (onde T3 vê x = 1, mas ainda não y = 1)
Isso está certo?
(6) Se um encadeamento puder sempre ler valores 'obsoletos', o que aconteceria se adotássemos o cenário típico de "publicação", mas em vez de sinalizar que alguns dados estão prontos, fazemos exatamente o contrário (exclua os dados)?
T1: delete gameObjectPtr; is_enabled.store(false, std::memory_order_release);
T2: while (is_enabled.load(std::memory_order_acquire)) gameObjectPtr->doSomething();
onde T2 ainda estaria usando um ptr excluído até ver que is_enabled é falso.
(7) Além disso, o fato de os threads poderem ler valores "obsoletos" significa que um mutex não pode ser implementado com apenas um direito atômico sem bloqueio? Isso exigiria um mecanismo de sincronização entre os threads. Exigiria um atômico com chave?