Je comprends que std::atomic<>
c'est un objet atomique. Mais dans quelle mesure atomique? À ma connaissance, une opération peut être atomique. Qu'entend-on exactement par rendre un objet atomique? Par exemple, s'il y a deux threads exécutant simultanément le code suivant:
a = a + 12;
Alors toute l'opération est-elle (disons add_twelve_to(int)
) atomique? Ou des modifications sont-elles apportées à la variable atomique (so operator=()
)?
c++
multithreading
c++11
atomic
curieux
la source
la source
a.fetch_add(12)
si vous voulez un RMW atomique.std::atomic
permet à la bibliothèque standard de décider de ce qui est nécessaire pour atteindre l'atomicité.std::atomic<T>
est un type qui permet des opérations atomiques. Cela n'améliore pas votre vie par magie, vous devez toujours savoir ce que vous voulez en faire. C'est pour un cas d'utilisation très spécifique, et les utilisations des opérations atomiques (sur l'objet) sont généralement très subtiles et doivent être pensées d'un point de vue non local. Donc, à moins que vous ne sachiez déjà cela et pourquoi vous voulez des opérations atomiques, le type n'est probablement pas d'une grande utilité pour vous.Réponses:
Chaque instanciation et spécialisation complète de std :: atomic <> représente un type sur lequel différents threads peuvent fonctionner simultanément (leurs instances), sans augmenter le comportement indéfini:
std::atomic<>
encapsule les opérations qui, à l'époque pré-C ++ 11, devaient être effectuées en utilisant (par exemple) des fonctions imbriquées avec MSVC ou des bultins atomiques dans le cas de GCC.En outre,
std::atomic<>
vous donne plus de contrôle en autorisant diverses commandes de mémoire qui spécifient des contraintes de synchronisation et de classement. Si vous souhaitez en savoir plus sur les atomes et le modèle de mémoire C ++ 11, ces liens peuvent être utiles:Notez que, pour les cas d'utilisation typiques, vous utiliseriez probablement des opérateurs arithmétiques surchargés ou un autre ensemble d'entre eux :
Étant donné que la syntaxe de l'opérateur ne vous permet pas de spécifier l'ordre de la mémoire, ces opérations seront effectuées avec
std::memory_order_seq_cst
, car il s'agit de l'ordre par défaut pour toutes les opérations atomiques en C ++ 11. Elle garantit la cohérence séquentielle (ordre global total) entre toutes les opérations atomiques.Dans certains cas, cependant, cela peut ne pas être requis (et rien n'est gratuit), vous pouvez donc utiliser une forme plus explicite:
Maintenant, votre exemple:
n'évaluera pas à un seul op atomique: cela entraînera
a.load()
(qui est atomique lui-même), puis une addition entre cette valeur et12
eta.store()
(également atomique) du résultat final. Comme je l'ai noté plus tôt,std::memory_order_seq_cst
sera utilisé ici.Cependant, si vous écrivez
a += 12
, ce sera une opération atomique (comme je l'ai déjà noté) et équivaut à peu près àa.fetch_add(12, std::memory_order_seq_cst)
.Quant à votre commentaire:
Votre déclaration n'est vraie que pour les architectures qui offrent une telle garantie d'atomicité pour les magasins et / ou les charges. Il existe des architectures qui ne font pas cela. En outre, il est généralement nécessaire que les opérations soient effectuées sur une adresse alignée mot / dword pour être atomique,
std::atomic<>
c'est quelque chose qui est garanti atomique sur chaque plate-forme, sans exigences supplémentaires. De plus, cela vous permet d'écrire du code comme celui-ci:Notez que la condition d'assertion sera toujours vraie (et donc ne se déclenchera jamais), vous pouvez donc toujours être sûr que les données sont prêtes après la
while
sortie de la boucle. C'est parce que:store()
à l'indicateur est exécuté après avoirsharedData
été défini (nous supposons quegenerateData()
renvoie toujours quelque chose d'utile, en particulier, ne retourne jamaisNULL
) et utilisestd::memory_order_release
order:sharedData
est utilisé après lawhile
sortie de la boucle, et donc après l'load()
indicateur from retournera une valeur non nulle.load()
utilise l'std::memory_order_acquire
ordre:Cela vous donne un contrôle précis sur la synchronisation et vous permet de spécifier explicitement comment votre code peut / ne peut pas / ne se comportera / ne se comportera pas. Cela ne serait pas possible si la seule garantie était l'atomicité elle-même. Surtout lorsqu'il s'agit de modèles de synchronisation très intéressants comme la commande de consommation de versions .
la source
int
s?std::atomic
(std::memory_order
) servent exactement à limiter les réorganisations autorisées.C'est une question de perspective ... vous ne pouvez pas l'appliquer à des objets arbitraires et faire en sorte que leurs opérations deviennent atomiques, mais les spécialisations fournies pour (la plupart) des types intégraux et des pointeurs peuvent être utilisées.
std::atomic<>
ne simplifie pas (utilisez des expressions de modèle pour) cela en une seule opération atomique, au lieu de cela, leoperator T() const volatile noexcept
membre fait un atomiqueload()
dea
, puis douze est ajouté, etoperator=(T t) noexcept
fait unstore(t)
.la source
int
ne garantit pas de manière portative que le changement est visible à partir d'autres threads, et sa lecture ne garantit pas non plus que vous voyez les changements d'autres threads, et certaines choses commemy_int += 3
ne sont pas garanties d'être effectuées de manière atomique à moins que vous n'utilisiezstd::atomic<>
- elles peuvent impliquer une séquence d'extraction, puis d'ajout, puis de stockage, dans laquelle un autre thread essayant de mettre à jour la même valeur peut entrer après l'extraction et avant le magasin, et clobber la mise à jour de votre thread.std::atomic
existe parce que de nombreux ISA ont une prise en charge matérielle directeCe que dit la norme C ++
std::atomic
a été analysé dans d'autres réponses.Voyons maintenant ce qui se
std::atomic
compile pour obtenir un autre type d'aperçu.Le principal point à retenir de cette expérience est que les processeurs modernes prennent directement en charge les opérations sur les entiers atomiques, par exemple le préfixe LOCK dans x86, et
std::atomic
existent essentiellement comme une interface portable pour ces instructions: que signifie l'instruction "lock" dans l'assemblage x86? Dans aarch64, LDADD serait utilisé.Cette prise en charge permet des alternatives plus rapides à des méthodes plus générales telles que
std::mutex
, qui peuvent rendre atomiques des sections multi-instructions plus complexes, au prix d'être plus lentes questd::atomic
parcestd::mutex
qu'elles effectuent desfutex
appels système sous Linux, qui est bien plus lent que les instructions de l'utilisateur émises parstd::atomic
, voir aussi: std :: mutex crée-t-il une clôture?Considérons le programme multi-thread suivant qui incrémente une variable globale sur plusieurs threads, avec différents mécanismes de synchronisation en fonction du préprocesseur défini est utilisé.
main.cpp
GitHub en amont .
Compilez, exécutez et démontez:
Sortie de condition de concurrence "incorrecte" extrêmement probable pour
main_fail.out
:et la sortie "droite" déterministe des autres:
Démontage de
main_fail.out
:Démontage de
main_std_atomic.out
:Démontage de
main_lock.out
:Conclusions:
la version non atomique enregistre le global dans un registre et incrémente le registre.
Par conséquent, à la fin, il est très probable que quatre écritures reviennent à global avec la même valeur «incorrecte» de
100000
.std::atomic
compile enlock addq
. Le préfixe LOCK permet d'inc
extraire, de modifier et de mettre à jour la mémoire de manière atomique.notre préfixe LOCK d'assembly en ligne explicite se compile presque de la même manière que
std::atomic
, sauf que ourinc
est utilisé à la place deadd
. Je ne sais pas pourquoi GCC a choisiadd
, étant donné que notre INC a généré un décodage de 1 octet plus petit.ARMv8 pourrait utiliser LDAXR + STLXR ou LDADD dans les processeurs plus récents: Comment démarrer des threads en C brut?
Testé dans Ubuntu 19.10 AMD64, GCC 9.2.1, Lenovo ThinkPad P51.
la source