Qu’est-ce que std :: atomic exactement?

Je comprends que std::atomic est un object atomique. Mais l’atome dans quelle mesure? À ma connaissance, une opération peut être atomique. Qu’entend-on exactement en faisant un object atomique? Par exemple, si deux threads exécutent simultanément le code suivant:

 a = a + 12; 

Alors l’opération entière (disons add_twelve_to(int) ) atomique? Ou les modifications apscopes à la variable atomique (donc operator=() )?

Chaque instanciation et spécialisation complète de std :: atomic <> représente un type, que différents threads peuvent fonctionner simultanément sur (leurs instances), sans augmenter le comportement indéfini:

Les objects de type atomique sont les seuls objects C ++ qui ne contiennent pas de courses de données. c’est-à-dire que si un thread écrit dans un object atomique alors qu’un autre thread le lit, le comportement est bien défini.

De plus, les access aux objects atomiques peuvent établir une synchronisation inter-thread et ordonner des access mémoire non atomiques comme spécifié par std::memory_order .

std::atomic<> wraps, qui en pré-C ++ 11 fois, devaient être effectuées en utilisant (par exemple) des fonctions verrouillées avec MSVC ou des bultins atomiques dans le cas de GCC.

De plus, std::atomic<> vous donne plus de contrôle en autorisant divers ordres de mémoire , qui spécifient les contraintes de synchronisation et de classement. Si vous voulez en savoir plus sur les modèles atomiques et mémoire C ++ 11, ces liens peuvent être utiles:

  • C ++ atomique et commande de mémoire
  • Comparaison: programmation sans locking avec atomique en C ++ 11 contre mutex et RW-locks
  • C ++ 11 a introduit un modèle de mémoire standardisé. Qu’est-ce que ça veut dire? Et comment cela va-t-il affecter la programmation C ++?
  • Concurrence en C ++ 11

Notez que pour les cas d’utilisation typiques, vous utiliserez probablement des opérateurs arithmétiques surchargés ou un autre ensemble :

 std::atomic value(0); value++; //This is an atomic op value += 5; //And so is this 

Comme la syntaxe opérateur ne vous permet pas de spécifier l’ordre 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. Il garantit une cohérence séquentielle opérations.

Dans certains cas, cependant, cela peut ne pas être requirejs (et rien n’est gratuit). Vous pouvez donc utiliser une forme plus explicite:

 std::atomic value {0}; value.fetch_add(1, std::memory_order_relaxed); // Atomic, but there are no synchronization or ordering constraints value.fetch_add(5, std::memory_order_release); // Atomic, performs 'release' operation 

Maintenant, votre exemple:

 a = a + 12; 

n’évalue pas en une seule opération atomique: il en résultera a.load() (qui est elle-même atomique), puis une addition entre cette valeur et 12 et a.store() (également atomique) du résultat final. Comme je l’ai noté précédemment, 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 approximativement à a.fetch_add(12, std::memory_order_seq_cst) .

En ce qui concerne votre commentaire:

Un int régulier a des charges atomiques et des magasins. Quel est le sharepoint l’envelopper avec atomic<> ?

Votre instruction n’est vraie que pour les architectures, qui offrent une telle garantie d’atomicité pour les magasins et / ou les charges. Il y a des architectures qui ne le font pas. En outre, il est généralement nécessaire que les opérations soient exécutées sur une adresse alignée mot / dword pour être atomique std::atomic<> est quelque chose, qui est garanti pour être atomique sur chaque plate-forme, sans exigences supplémentaires. De plus, il vous permet d’écrire du code comme ceci:

 void* sharedData = nullptr; std::atomic ready_flag = 0; // Thread 1 void produce() { sharedData = generateData(); ready_flag.store(1, std::memory_order_release); } // Thread 2 void consume() { while (ready_flag.load(std::memory_order_acquire) == 0) { std::this_thread::yield(); } assert(sharedData != nullptr); // will never sortinggger processData(sharedData); } 

Notez que cette condition d’assertion sera toujours vraie (et donc ne se déclenchera jamais), vous pouvez donc toujours être sûr que ces données sont prêtes après une fermeture de boucle. C’est parce que:

  • store() à l’indicateur est effectuée après que sharedData est défini (nous supposons que generateData() retourne toujours quelque chose d’utile, en particulier, ne retourne jamais NULL ) et utilise l’ordre std::memory_order_release :

memory_order_release

Une opération de magasin avec cet ordre de mémoire effectue l’opération de libération : aucune lecture ou écriture dans le thread en cours ne peut être réorganisée après ce magasin. Toutes les écritures dans le thread en cours sont visibles dans d’autres threads qui acquièrent la même variable atomique

  • sharedData est utilisé après une boucle while et, par conséquent, après load() partir de l’ sharedData flag renvoie une valeur différente de zéro. load() utilise std::memory_order_acquire ordre:

std::memory_order_acquire

Une opération de chargement avec cet ordre de mémoire effectue l’opération d’ acquisition sur l’emplacement de mémoire affecté: aucune lecture ou écriture dans le thread en cours ne peut être réorganisée avant ce chargement. Toutes les écritures dans d’autres threads qui libèrent la même variable atomique sont visibles dans le thread en cours .

Cela vous donne un contrôle précis sur la synchronisation et vous permet de spécifier explicitement comment votre code peut / peut ne pas / va / ne se comportera pas. Cela ne serait pas possible si la seule garantie était l’atomicité même. Surtout quand il s’agit de modèles de synchronisation très intéressants comme la commande release-consume .

Je comprends que std::atomic<> fait un object atomique.

C’est une question de perspective … vous ne pouvez pas l’appliquer à des objects 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.

a = a + 12;

std::atomic<> (utilisez des expressions de modèle pour) simplifie cela en une seule opération atomique, au lieu de cela l’ operator T() const volatile noexcept member effectue un load() atomique load() de a , alors douze est ajouté, et operator=(T t) noexcept fait un store(t) .