Eu entendo que std::atomic<>
é um objeto atômico. Mas atômica até que ponto? Para meu entendimento, uma operação pode ser atômica. O que exatamente se entende por tornar um objeto atômico? Por exemplo, se houver dois threads executando simultaneamente o seguinte código:
a = a + 12;
Então toda a operação (digamos add_twelve_to(int)
) é atômica? Ou são feitas alterações na variável atômica (so operator=()
)?
c++
multithreading
c++11
atomic
curiousguy
fonte
fonte
a.fetch_add(12)
se quiser um RMW atômico.std::atomic
permite que a biblioteca padrão decida o que é necessário para atingir a atomicidade.std::atomic<T>
é um tipo que permite operações atômicas. Não magicamente melhora sua vida, você ainda precisa saber o que quer fazer com ela. É para um caso de uso muito específico, e o uso de operações atômicas (no objeto) geralmente é muito sutil e precisa ser pensado de uma perspectiva não local. Portanto, a menos que você já saiba disso e por que deseja operações atômicas, o tipo provavelmente não será muito útil para você.Respostas:
Cada instanciação e especialização completa de std :: atomic <> representa um tipo no qual diferentes threads podem operar simultaneamente (suas instâncias), sem aumentar o comportamento indefinido:
std::atomic<>
envolve operações que, em pré-C ++ 11 vezes, tiveram que ser executadas usando (por exemplo) funções intertravadas com MSVC ou bultins atômicos no caso de GCC.Além disso,
std::atomic<>
oferece mais controle, permitindo várias ordens de memória que especificam restrições de sincronização e ordem. Se você quiser ler mais sobre o modelo atômico e de memória C ++ 11, esses links podem ser úteis:Observe que, para casos de uso típicos, você provavelmente usaria operadores aritméticos sobrecarregados ou outro conjunto deles :
Como a sintaxe do operador não permite que você especifique a ordem da memória, essas operações serão executadas com
std::memory_order_seq_cst
, pois esta é a ordem padrão para todas as operações atômicas no C ++ 11. Ele garante consistência sequencial (total global de pedidos) entre todas as operações atômicas.Em alguns casos, no entanto, isso pode não ser necessário (e nada vem de graça), portanto, você pode usar uma forma mais explícita:
Agora, seu exemplo:
não avaliará uma única operação atômica: resultará em
a.load()
(que é atômico propriamente dito), depois adição entre esse valor12
ea.store()
(também atômico) do resultado final. Como observei anteriormente,std::memory_order_seq_cst
será usado aqui.No entanto, se você escrever
a += 12
, será uma operação atômica (como observei antes) e é aproximadamente equivalente aa.fetch_add(12, std::memory_order_seq_cst)
.Quanto ao seu comentário:
Sua declaração é verdadeira apenas para arquiteturas que fornecem essa garantia de atomicidade para lojas e / ou cargas. Existem arquiteturas que não fazem isso. Além disso, geralmente é necessário que as operações sejam executadas no endereço alinhado por palavra / palavra a ser atômico,
std::atomic<>
algo que é garantido como atômico em todas as plataformas, sem requisitos adicionais. Além disso, permite escrever código como este:Observe que a condição de asserção sempre será verdadeira (e, portanto, nunca será acionada), para que você sempre tenha certeza de que os dados estão prontos após
while
a saída do loop. Isso é porque:store()
para o sinalizador é executado depois desharedData
definido (assumimos quegenerateData()
sempre retorna algo útil, em particular, nunca retornaNULL
) e usa astd::memory_order_release
ordem:sharedData
é usado apóswhile
a saída do loop e, portanto, após oload()
sinalizador retornará um valor diferente de zero.load()
usastd::memory_order_acquire
ordem:Isso fornece controle preciso sobre a sincronização e permite que você especifique explicitamente como seu código pode / pode ou não / não / se comportará. Isso não seria possível se apenas a garantia fosse a própria atomicidade. Especialmente quando se trata de modelos de sincronização muito interessantes, como a ordem de liberação / consumo .
fonte
int
s?std::atomic
(std::memory_order
) servem exatamente para a finalidade de limitar as reordenações que são permitidas.Essa é uma questão de perspectiva ... você não pode aplicá-lo a objetos arbitrários e fazer com que suas operações se tornem atômicas, mas as especializações fornecidas para (a maioria) tipos integrais e ponteiros podem ser usadas.
std::atomic<>
não (usa expressões de modelo para) simplifica isso para uma única operação atômica; em vez disso, ooperator T() const volatile noexcept
membro faz um atômicoload()
dea
, então doze são adicionados eoperator=(T t) noexcept
faz astore(t)
.fonte
int
não garante portabilidade que a alteração seja visível em outros threads, nem a leitura garante que você veja as alterações de outros threads, e algumas coisas comomy_int += 3
não são garantidas que sejam feitas atomicamente, a menos que você usestd::atomic<>
- elas podem envolver uma busca e, em seguida, adicione e, em seguida, sequência de armazenamento, em que algum outro encadeamento que tente atualizar o mesmo valor possa aparecer após a busca e antes da loja e prejudicar a atualização do encadeamento.std::atomic
existe porque muitos ISAs têm suporte direto de hardware para eleO que o padrão C ++ diz sobre
std::atomic
foi analisado em outras respostas.Então agora vamos ver o que
std::atomic
compila para obter um tipo diferente de insight.O principal argumento desse experimento é que as CPUs modernas têm suporte direto para operações inteiras atômicas, por exemplo, o prefixo LOCK em x86, e
std::atomic
existem basicamente como uma interface portátil para essas instruções : O que a instrução "lock" significa na montagem do x86? No aarch64, o LDADD seria usado.Esse suporte permite alternativas mais rápidas a métodos mais gerais, como
std::mutex
, o que pode tornar mais complexas as seções multi-instruções atômicas, ao custo de ser mais lento do questd::atomic
porquestd::mutex
fazfutex
chamadas de sistema no Linux, que é muito mais lento que as instruções da terra do usuário emitidas porstd::atomic
, veja também: std :: mutex cria uma cerca?Vamos considerar o seguinte programa multiencadeado que incrementa uma variável global em vários encadeamentos, com diferentes mecanismos de sincronização, dependendo de qual definição do pré-processador é usada.
main.cpp
GitHub upstream .
Compilar, executar e desmontar:
Saída de condição de corrida "incorreta" extremamente provável para
main_fail.out
:e saída "certa" determinística dos outros:
Desmontagem de
main_fail.out
:Desmontagem de
main_std_atomic.out
:Desmontagem de
main_lock.out
:Conclusões:
a versão não atômica salva o global em um registro e incrementa o registro.
Portanto, no final, muito provavelmente quatro gravações retornam ao global com o mesmo valor "errado" de
100000
.std::atomic
compila paralock addq
. O prefixo LOCK faz a seguinteinc
busca, modificação e atualização da memória atomicamente.nosso prefixo LOCK explícito do assembly embutido é compilado quase da mesma maneira que
std::atomic
, exceto que o nossoinc
é usado em vez deadd
. Não sei por que o GCC escolheuadd
, considerando que nosso INC gerou uma decodificação 1 byte menor.O ARMv8 poderia usar LDAXR + STLXR ou LDADD em CPUs mais recentes: Como inicio threads em C simples?
Testado no Ubuntu 19.10 AMD64, GCC 9.2.1, Lenovo ThinkPad P51.
fonte