É seguro ler um ponteiro para um atomic sem sincronização, supondo que ele nunca será um nullptr? Como no código a seguir, suponha que há duas threads em execução writer
e reader
simultaneamente.
std::atomic<int>* g_atomic = new std::atomic<int>{};
void writer()
{
for (int i = 0; i < 101; i++)
{
auto* new_atomic = new std::atomic<int>{i};
std::atomic_thread_fence(std::memory_order_seq_cst); // memory barrier.
g_atomic = new_atomic; // ignore the memory leak
}
}
void reader()
{
auto value = g_atomic->load();
while (value < 100)
{
assert(value >= 0 && value <= 100);
value = g_atomic->load();
}
}
por seguro quero dizer que sempre lerei um valor de 0 a 100, não lerei um ponteiro inválido nem lerei o objeto apontado antes de sua inicialização.
minha intuição me diz que isso é seguro porque
- ponteiros são lidos ou escritos atomicamente em todas as arquiteturas.
- o valor apontado é lido atomicamente, ele precisa ser buscado na RAM, e a barreira de memória antes da gravação garante que a RAM esteja sempre correta.
Então, isso é seguro? Talvez apenas em todas as arquiteturas comuns?
Sim, em assembly. Mas você está escrevendo em C++, onde o que importa é o modelo de memória da linguagem e sua máquina abstrata. Seu código tem UB de corrida de dados de escrita+leitura simultâneas e não sincronizadas em
g_atomic
.Na prática, o loop do leitor retirará a carga
g_atomic
do seu loop. Como não é atômico, seria UB para outra thread alterá-lo, o que o compilador pode presumir que não acontece. Em outras palavras, criar corridas de dados em variáveis não atômicas UB é o que permite que os compiladores continuem a fazer otimizações importantes para código single-threaded. (Eles não sabem quais globais são compartilhados ou não.)Em C++, se você quiser o comportamento esperado, precisa solicitá-lo com
std::atomic< T* >
.Com
relaxed
store e load se não precisar de nenhuma ordenação.Mas você realmente precisa ordenar para não publicar o ponteiro antes que o objeto apontado esteja totalmente construído. Você está usando um método lento
thread_fence(seq_cst)
para conseguir isso; na verdade, você só precisa derelease
, de preferência.store(val, release)
em vez de uma cerca separada, para que ele possa ser compilado para AArch64stlr
em vez de uma instrução de barreira separada.No seu caso,
T
éstd::atomic<int>
também conhecido comostd::atomic_int
. Então, seu código deve ser escrito assim para compilar no conjunto que você esperava obter do original. Renomeeig_atomic
parag_ptr
porque ele e o apontador precisam ser atômicos.( Na verdade, você não precisa que os objetos apontados sejam atômicos para que isso seja seguro. Então
std::atomic< int* > g_ptr;
, supondo que você não terá threads escrevendo esses objetos apontados. A inicialização acontece antes da leitura, graças à sincronização entre o escritor e o leitor. Não é verdade formalmente aqui, pois useirelaxed
no leitor porqueconsume
está obsoleto e isso compila para o conjunto correto na prática.)( Godbolt )
Sim, porque é
std::atomic<int>
.Bem, da memória compartilhada, então provavelmente do cache. O cache é coerente entre os núcleos pelos quais std::thread é executado, em todas as implementações C++ do mundo real. (Em placas com núcleos que compartilham memória, mas não têm coerência de cache, por exemplo, ARM DSP + microcontrolador, você não executa threads do mesmo programa nesses núcleos.)
Em C++, você precisa formalmente de pelo menos
std::memory_order_consume
, mas ele foi descontinuado e removido, pois a definição original era muito complicada de implementar. Os compiladores só o implementaram promovendo-o paraacquire
, sem realmente aproveitar a ordenação de dependências de memória que todas as ISAs, exceto DEC Alpha, oferecem.Neste caso, não há uma maneira plausível para um compilador descobrir qual valor o ponteiro load deve ter produzido, então ele precisa criar um conjunto que carregue e desreferencia o ponteiro. Portanto, há uma dependência de dados entre o primeiro
pointer.load(relaxed)
etmp->load(relaxed)
, e ISAs reais garantem que não pode haver reordenação de LoadLoad nesse caso. Na prática, funciona; o kernel Linux usa isso para RCU, por exemplo, para evitar quaisquer barreiras de memória em caminhos de leitura.Veja C++11: a diferença entre memory_order_relaxed e memory_order_consume e respostas semelhantes sobre
consume
ordenação de dependências de memória.Não, isso não é seguro.
g_atomic
Deveria serstd::atomic<std::atomic<int>*>
. Um ponteiro atômico para um objeto atômico.Mesmo que o hardware subjacente não interrompa leituras/gravações do tamanho de um ponteiro, já que seu ponteiro não é atômico nem volátil, o compilador está livre para otimizar todas as cargas/armazenamentos do valor do ponteiro, exceto a última gravação no último loop e a primeira carga no leitor.
Seu leitor provavelmente entrará em loop infinito, pois ele continua verificando o mesmo ponteiro alocado antigo em vez de pegar a próxima alocação.
Quanto ao valor armazenado no
atomic<int>
, a norma diz:Portanto, o ponteiro atômico deve ser acessado com uma ordem de memória de aquisição/liberação. Sua cerca mais uma operação relaxada também deve funcionar (se combinada com uma cerca no lado do leitor), mas é mais cara do que o necessário.