O que é garantido com o C ++ std :: atomic no nível do programador?

9

Ouvi e li vários artigos, palestras e perguntas sobre o stackoverflow std::atomice 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::atomicNÃ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?

Albert Caldas
fonte

Respostas:

3
  1. Sim, não há corridas de dados
  2. Sim, com memory_ordervalores apropriados , você pode garantir consistência sequencial
  3. Uma leitura-modificação-gravação atômica sempre ocorrerá inteiramente antes ou inteiramente após uma gravação atômica na mesma variável
  4. Sim, T2 pode ler um valor obsoleto de uma variável após uma gravação atômica em T1

As operações atômicas de leitura, modificação e gravação são especificadas de maneira a garantir sua atomicidade. Se outro encadeamento puder gravar no valor após a leitura inicial e antes da gravação de uma operação RMW, essa operação não será atômica.

Os encadeamentos sempre podem ler valores obsoletos, exceto quando acontece antes, garante a ordem relativa .

Se uma operação RMW lê um valor "obsoleto", garante que a gravação gerada será visível antes de qualquer gravação de outros encadeamentos que substituam o valor lido.

Atualizar por exemplo

Se T1 gravar x=1e T2 fizer x++, com xinicialmente 0, as opções do ponto de vista do armazenamento de xserão:

  1. A gravação de T1 é a primeira, então T1 grava x=1, depois T2 lê x==1, aumenta para 2 e grava de volta x=2como uma única operação atômica.

  2. A gravação de T1 é a segunda. T2 lê x==0, incrementa para 1 e grava de volta x=1como uma única operação, depois T1 grava x=1.

No entanto, desde que não haja outros pontos de sincronização entre esses dois threads, os threads podem continuar com as operações que não foram liberadas na memória.

Assim, T1 pode emitir x=1, e prosseguir com outras coisas, mesmo que T2 ainda leia x==0(e, assim, escreva x=1).

Se houver outros pontos de sincronização, ficará aparente qual thread modificado xprimeiro, porque esses pontos de sincronização forçarão um pedido.

Isso é mais aparente se você tiver uma condição do valor lido de uma operação RMW.

Atualização 2

  1. Se você usar memory_order_seq_cst(o padrão) para todas as operações atômicas, não precisará se preocupar com esse tipo de coisa. Do ponto de vista do programa, se você vir "msg", o T1 será executado, depois o T3 e o T2.

Se você usar outros pedidos de memória (especialmente memory_order_relaxed), poderá ver outros cenários no seu código.

  1. Nesse caso, você tem um erro. Suponha que a is_enabledflag seja verdadeira, quando T2 entra em seu whileloop, então decide executar o corpo. T1 agora exclui os dados e T2, em seguida, adia o ponteiro, que é um ponteiro pendente, e o comportamento indefinido se segue. Os atômicos não ajudam ou atrapalham de maneira alguma além de impedir a corrida de dados na bandeira.

  2. Você pode implementar um mutex com uma única variável atômica.

Anthony Williams
fonte
Muito obrigado à @Anthony Wiliams pela sua resposta rápida. Atualizei minha pergunta com um exemplo de RMW lendo um valor "obsoleto". Olhando para este exemplo, o que você quer dizer com ordenação relativa e que o W (1) do T2 estará visível antes de qualquer gravação? Isso significa que, uma vez que T2 tenha visto as alterações de T1, ele não lerá mais W (1) de T2?
Albert Caldas
Portanto, se "Threads sempre podem ler valores obsoletos", significa que a coerência do cache nunca é garantida (pelo menos no nível do programador c ++). Você poderia dar uma olhada na minha atualização2, por favor?
Albert Caldas
Agora vejo que eu deveria ter prestado mais atenção aos modelos de linguagem e memória de hardware para entender completamente tudo isso, essa era a peça que estava faltando. Muito obrigado!
Albert Caldas
1

Em relação a (3) - depende da ordem da memória utilizada. Se ambos, a loja e a operação RMW usarem std::memory_order_seq_cst, as duas operações serão ordenadas de alguma maneira - ou seja, a loja ocorrerá antes da RMW ou o contrário. Se a loja encomendar antes do RMW, é garantido que a operação RMW "veja" o valor que foi armazenado. Se a loja for solicitada após o RMW, ela substituirá o valor gravado pela operação RMW.

Se você usar ordens de memória mais relaxadas, as modificações ainda serão solicitadas de alguma forma (a ordem de modificação da variável), mas você não tem garantias de que o RMW "veja" o valor da operação de armazenamento - mesmo se a operação RMW é a ordem após a gravação na ordem de modificação da variável.

Caso você queira ler mais um artigo, posso consultá-lo em Modelos de Memória para Programadores C / C ++ .

mpoeter
fonte
Obrigado pelo artigo, eu ainda não o tinha lido. Mesmo que seja bastante antigo, tem sido útil reunir minhas idéias.
Albert Caldas
11
Fico feliz em ouvir isso - este artigo é um capítulo um pouco ampliado e revisado da minha tese de mestrado. :-) Ele se concentra no modelo de memória, como apresentado no C ++ 11; Eu posso atualizá-lo para refletir as (pequenas) alterações introduzidas no C ++ 14/17. Entre em contato se tiver algum comentário ou sugestão de melhorias!
mpoeter 01/02