Bref – opérations atomiques primitives

Le nom le dit : ceci n'est qu'un bref, pas une description exhaustive du sujet.

Ce petit document se base sur Tanenbaum, pp. 124-125;131.

Ce qui suit décrit très brièvement quelques-unes des opérations atomiques primitives susceptibles d'être offertes sur des architectures multi-coeurs contemporaines.

Ces opérations ont été pensées à titre de solutions à ce qu'on nomme le problème du consensus, à savoir comment faire en sorte que processus concurrents conviennent d'un même état, par exemple pour prendre une décision comme celle de persister une transaction.

Le Test and Set Lock TSL

L'opération TSL s'utilise selon la forme proposée à droite. Dans cette écriture :

  • Le terme Rx décrit un registre; et
  • Le terme LOCK décrit une adresse en mémoire.

Cette expression a le sens suivant : lire le contenu de LOCK et le déposer dans Rx, puis écrire une valeur non-nulle dans LOCK. Cette paire d'opérations se fait de manière atomique, au sens où aucun autre processeur et aucune autre coeur ne peut accéder à LOCK pendant ce temps.

TSL Rx,LOCK

L'atomicité de TSL implique un verrouillage du bus de mémoire pendant l'exécution de cette instruction. C'est donc une opération dispendieuse, comme le sont en général les opérations atomiques.

Un exemple d'utilisation typique (pseudocode assembleur) est proposé à droite. La partie enter_region itère jusqu'à ce qu'il soit clair que le verrou représenté par la zone LOCK ait été obtenu; la partie leave_region, qui suppose que LOCK soit possédé au préalable, est évidemment plus simple.

Visiblement, il est possible avec TSL d'implémenter une section critique.

enter_region:
   TSL Rx,LOCK
   CMP Rx,#0
   JNE enter_region
   RET

leave_region:
   MOVE LOCK,#0
   RET

Le TSL permet de solutionner le problème du consensus pour un nombre fini de processus concurrents. L'opération Compare and Exchange, survolée plus bas, est une approche équivalente mais dont la portée un peu plus générale que celle de TSL.

Pour plus d'informations, voir http://en.wikipedia.org/wiki/Test-and-set

Plusieurs écrivent aussi Compare-and-Swap, ou CAS.

Le Compare and Exchange XCHG

Sur les architectures contemporaines, plutôt que TSL, on retrouve typiquement une implémentation de Compare and Exchange (nommée XCHG sur les architectures Intel).

L'opération XCHG s'utilise selon la forme proposée à droite. Dans cette écriture :

  • Le terme Rx décrit un registre, et
  • Le terme LOCK décrit une adresse en mémoire

Cette expression permute de manière atomique le contenu de LOCK et celui de Rx. Cette opération se fait de manière atomique, au sens où aucun autre processeur et aucune autre coeur ne peut accéder à LOCK pendant ce temps. Conséquemment, suite à l'opération, Rx contient la valeur qui se trouvait précédemment dans LOCK; c'est sur cette base que s'exprime la programmation à l'aide de l'opération XCHG.

XCHG Rx,LOCK

L'atomicité de XCHG implique un verrouillage du bus de mémoire pendant l'exécution de cette instruction. C'est donc une opération dispendieuse, comme le sont en général les opérations atomiques.

Un exemple d'utilisation typique (pseudocode assembleur) est proposé à droite. Ici, nous avons explicitement choisi de représenter par 1 un verrou saisi et 0 un verrou disponible.

La partie enter_region itère jusqu'à ce qu'il soit clair que le verrou représenté par la zone LOCK ait été obtenu. L'idée est, essentiellement :

  • Je dépose 1 dans LOCK et je vérifie que LOCK contenait 0 auparavant (donc que personne n'était dans la région visée)
  • Si c'est le cas, alors je suis celui qui possède le droit d'accès à cette région
  • Dans le cas contraire, quelqu'un y était déjà alors je ne peux y aller (mieux vaut essayer à nouveau plus tard)

La partie leave_region, qui suppose que LOCK soit possédé au préalable, est évidemment plus simple.

Visiblement, il est possible avec XCHG d'implémenter une section critique. Il est aussi possible d'implémenter plusieurs autres mécanismes de synchronisation à partir de cette opération. La mécanique générale est essentiellement toujours la même :

  • Lire la valeur à une certaine adresse LOCK. Cette valeur est une sorte d'invariant qui doit tenir pour l'ensemble de notre opération
  • Calculer une nouvelle valeur , peut-être à partir de
  • Essayer de permuter de manière atomique le contenu de LOCK avec en s'assurant que LOCK contenait encore (c'est ce que permet l'atomicité de XCHG), donc que l'invariant tient toujours
  • Si l'on constate que LOCK ne contenait plus , donc que l'invariant ne tient plus, alors on recommence
enter_region:
   MOVE Rx,#1
   XCHG Rx,LOCK
   CMP Rx,#0
   JNE enter_region
   RET

leave_region:
   MOVE LOCK,#0
   RET

Comme cela a été mentionné à quelques reprises dans le cours, la permutation de deux états est une opération fondamentale, sans doute la plus importante en programmation.

Risques d'un ABA

Vous remarquerez peut-être un risque avec XCHG, soit le fait qu'il est possible avec certains algorithmes qu'entre le moment où l'ancienne valeur de LOCK est lue par un processus et le moment où l'écriture dans LOCK est réalisée par , un autre processus ait modifié deux fois l'état de LOCK.

Supposant que LOCK ait valu lors de sa lecture par , puis que l'ait fait passer à puis à à nouveau avant que n'ait procédé à sa propre écriture dans LOCK, le double changement de LOCK pourrait passer inaperçu pour .

C'est ce qu'on nomme le problème ABA. Il existe diverses solutions, heureusement, que vous pourrez lire si vous êtes curieuses ou curieux.

Dans son livre Concurrency in Action, Anthony Williams suggère que la technique la plus simple d'éviter un ABA soit d'associer un compteur à la donnée sur laquelle le Compare-Exchange est réalisé (sur la moitié des bits de l'entier manipulé, par exemple). Ceci fait en sorte que l'opération « échouera » même si elle aurait, en temps normal, réussi, du moins si un tiers réalise deux permutations pour provoquer un ABA entre-temps.

Pour plus de détails, voir http://en.cppreference.com/w/cpp/atomic/atomic/compare_exchange, ou encore http://www.boost.org/doc/libs/1_54_0/doc/html/atomic/interface.html pour la version de Boost.

En C++

Avec C++, les opérations de type Compare-Exchange se font., sans surprise, sur des atomiques, et s'expriment sous forme de méthodes (il existe aussi des versions de ces services sous forme de fonction globale).

Elle se déclinent en quelques grandes familles :

Pour exchange(), la signature est...

template <class T>
   T std::atomic<T>::exchange(T desired, std::memory_order = std::memory_order_seq_cst);

Cette méthode permute atomiquement la valeur de *this avec celle de desired.

Pour compare_exchange_*, les signatures possibles sont les mêmes (au nom près) dans chaque cas; la nuance entre les déclinaisons *_weak() et *_strong() est que celles portant le suffixe _weak() sont typiquement un peu plus rapide que celles portant le suffixe _strong(), du moins sur certaines architectures matérielles, mais peut livrer de faux négatifs (retourner false même lors d'un succès).

Prenant pour exemple compare_exchange_weak(), nous avons...

template <class T>
   bool std::atomic<T>::compare_exchange_weak(
      T &expected, T desired,
      std::memory_order = std::memory_order_seq_cst
   );
template <class T>
   bool std::atomic<T>::compare_exchange_weak(
      T &expected, T desired,
      std::memory_order success, std::memory_order failure
   );

...de même que des déclinaisons permettant d'expliciter les contraintes de cohérence lors d'un succès et lors d'un échec (par défaut, la méthode utilise std::memory_order_seq_cst et garantit la cohérence séquentielle... en l'absence de conditions de course, bien entendu). Dans le cas où les contraintes de cohérence sont spécifiées individuellement pour un succès et un échec, certaines règles s'appliquent :

Ces méthodes sont déclinées avec et sans qualification volatile. Les compare_exchange_* retournent true seulement dans le cas d'un succès. La méthode exchange() retourne la valeur de la variable atomique avant l'appel.

Le sens d'un compare_exchange_* est de comparer la valeur de *this avec celle de expected; si elles sont égales, *this prendra la valeur de desired, sinon expected prendra la valeur de *this. Un exemple d'utilisation pour l'ajout d'un élément sur une pile implémentée sans verrous, tiré de Concurrency in Action, suit :

#include <atomic>
template <class T>
   class pile_sans_verrous { // ebauche
   public:
      using value_type = T;
   private:
      struct noeud {
         noeud *pred;
         value_type valeur;
         noeud(const value_type &valeur)
            : valeur{valeur}, pred{}
         {
         }
      };
      std::atomic<noeud*> tete;
   public:
      void push(const value_type &val) {
         auto nouveau = new noeud{val};
         nouveau->pred = tete.load();
         while(!tete.compare_exchange_weak(nouveau->pred, nouveau))
            ;
      }
      // ...
   };

Examinez la méthode push() :

Selon vous, cette implémentation est-elle sensible à un ABA?

Implémenter un mutex

Sachant ce que sont les opérations atomiques primitives, il est possible d'examiner comment peuvent être implémentés les mutex par un système d'exploitation. Ce qui suit n'est qu'une illustration, car chaque plateforme a ses propres outils et ses propres particularités, et se limite au User Space pour fins de simplicité. En ce sens, c'est plus près d'une section critique ou d'un futex que d'un mutex.

Le pseudocode assembleur à droite utilise un registre Rx et une adresse MUTEX qu'on présumera celle d'un entier de la taille du mot mémoire. L'exemple est exprimé avec un TSL mais en pratique, il est nettement plus probable que XCHG soit utilisé.

L'étape mutex_lock cherche à verrouiller MUTEX. Si la tentative réussit, on saute à ok et on quitte. Si la tentative échoue, on appelle thread_yield pour ne pas consommer tout le temps du processeur et, au retour, on essaie à nouveau de verrouiller MUTEX.

L'étape mutex_unlock, comme c'est souvent le cas, est plus simple. Évidemment, ce code présume que MUTEX ait été verrouillé au préalable.

mutex_lock:
   TSL Rx,MUTEX
   CMP Rx,#0
   JZE ok
   CALL thread_yield
   JMP mutex_lock
ok:
   RET

mutex_unlock:
   MOVE MUTEX,#0
   RET

Je vous invite à essayer d'implémenter la même chose avec XCHG, bien entendu.

Avec C++ et une atomique

Il est possible d'implémenter un mutex (sous forme d'un Spin Lock) avec une atomique. Ce qui suit est tiré de Concurrency in Action :

#include <atomic>
class mutex_spinlock {
   std::atomic_flag fanion;
public:
   mutex_spinlock()
      : fanion{ ATOMIC_FLAG_INIT }
   {
   }
   void lock() {
      while(fanion.test_and_set(std::memory_order_acquire))
         ;
   }
   void unlock() {
      fanion.clear(std::memory_order_release);
   }
};

Le std::atomic_flag est l'entité la plus primitive du zoo atomique de C++. De tous les types atomiques, c'est le seul pour lequel le standard garantit une implémentation vraiment sans verrous (une « pure atomique »). On ne peut presque rien faire avec ce type :

Comme vous pouvez le constater, cette petite gamme de services suffit à implémenter un mutex.

Atomicité – pédagogie

Si votre compilateur C ou C++ ne supporte pas encore les variables atomiques, alors les bibliothèques http://mintomic.github.io/ et http://www.hpl.hp.com/research/linux/atomic_ops/ peuvent vous rendre de précieux services.

L'atomicité est un concept de bas niveau, mais nécessaire de la multiprogrammation à l'aide de processeurs contemporains. Avec ce concept, nous touchons à la fois à la question des opérations indivises du point de vue logique et celle des contraintes de réordonnancement des opérations par les processeurs et leurs coeurs. Ces questions sont plus qu'importantes; en effet :

L'atomicité est donc un requis pour assurer la cohérence séquentielle des programmes; dans un système multiprogrammé, sans atomicité, il n'est parfois pas possible de tirer du sens des sources du programme.

Les opérations atomiques sont celles qui ont été définies pour le langage dû à leur correspondance avec ce que peut offrir le substrat matériel. Par exemple (merci à JF Bastien pour ce qui suit, pris de https://github.com/jfbastien/no-sane-compiler) :

Le type std::atomic<T>

De prime abord, C++ offre de manière standard une version de std::atomic<T> pour les types T suivants :

Il est possible d'utiliser le template std::atomic<T> pour d'autres types T que ceux-ci, dans la mesure où le type T en question est TriviallyCopyable, donc ses instances peuvent être dupliquées par une vulgaire copie bit à bit. Un std::atomic<T> n'est ni copiable, ni déplaçable; c'est une objet à manipuler avec soin.

Outre le type std::atomic_flag, qui est implémenté sans verrous sur toutes les plateformes (c'est une exigence du standard), tout les autres types std::atomic<T> peuvent être soutenus par une forme de verrou dans leur implémentation, surtout si le type T occupe plus d'espace en mémoire que ne le font les types primitifs.

La fonction std::atomic_is_lock_free() permet de vérifier si un objet donné est implémenté (ou non) sans verrous; de plus, à partir de C++ 17, std::is_always_lock_free (qui est constexpr) permettra de savoir si un type donné est toujours implémenté sans verrous sur la plateforme choisie.

De plus, les

Modèles de cohérence

Le terme anglais Memory Ordering Constraint, que j'ai traduit un peu librement ici par « modèle de cohérence », décrit un ensemble de règles contraignant les réordonnancements permis au compilateur et au processeur lors d'opérations sur une atomique. Les modèles de cohérence sont applicables à la fois sur une variable, et sur les opérations prises sur une base individuelle.

Avec C++, les modèles de cohérence possibles sont les suivants.

NomLecture seule?Écriture seule? Lecture-Modification-Écriture?Concept modélisé

memory_order_seq_cst

X X X SC-DRF

memory_order_acq_rel

  XAcquire-Release

memory_order_release

 X X Acquire-Release

memory_order_acquire

X  XAcquire-Release

memory_order_consume

X  XAcquire-Release (ne lui touchez pas pour le moment; il sera déprécié avec C++ 17, pour être retravaillé et réintroduit ultérieurement)

memory_order_relaxed

X X X Relaxed

Les modèles de cohérence permettent de décrire les relations de synchronisation primitives dans un programme multiprogrammé.

SC-DRF – Séquentiellement cohérent en l'absence de conditions de course

Ce modèle est typiquement le comportement souhaité d'un programme, mais selon les architectures, il est possible qu'il soit trop dispendieux à atteindre pour certains programmes. On associe ce comportement au modèle de cohérence memory_order_seq_cst, qui est le modèle par défaut des opérations atomiques.

En gros, une opération atomique respectant ce modèle ne pourra être réordonnancée par le processeur. Les opérations qui la précèdent dans le code source continueront de la précéder à l'exécution, et les opérations qui la suivent dans le code source la suivront aussi à l'exécution.

Dans un programme SC-DRF, tous les threads voient les écritures aux données partagées se produire dans un même ordre.

Description informelle

Pour se faire une image de la cohérence séquentielle, supposons ceci :

x ← a + b; // A
y ← c + d; // B
x ← a + b; // C
y ← x + c; // D

Ce dernier exemple montre une dépendance de données (Data Dependency), au sens où dépend du résultat de pour être raisonnable. Maintenant, il faut comprendre que quand un programme n'a qu'un seul thread, le compilateur (et par la suite, le processeur) voit la séquence entière d'exécution et peut déterminer un ordre raisonnable d'exécution qui soit efficace et qui respecte la cohérence de ce que décrit le code source.

Quand un programme n'est pas séquentiellement cohérent, cette garantie, qui nous permet de raisonner sur la base du code source pour évaluer ce que sera le comportement du programme, disparait. Il arrive qu'on puisse s'en accommoder, dans des circonstances pointues et pour des raisons de vitesse, mais en général on veut que nos programmes soient SC-DRF (séquentiellement cohérent en l'absence de conditions de course). C'est la garantie qu'offrent, par défaut, les langages contemporains (C++, mais aussi Java, C et C#).

Si des Data Races sont introduites dans un programme, la garantie SC s'en va et nous sommes sérieusement dans le pétrin.

 En général, une Data Race, c'est simple à définir :

Par « objet » ici, on entend une zone mémoire capable d'entreposer une valeur, pas seulement des trucs complexes comme des classes ou des instances de ces classes. Au sens de cette définition du terme « objet », en fait, un humble int est un objet.

Acquire-Release

Verrouiller un mutex est une opération Acquire, alors que le libérer est une opération Release sur la même variable

Ce modèle ne garantit pas d'ordonnancement total sur les écritures aux variables partagées d'un programme, mais permet des synchronisations plus locales, entre paires d'opérations.

Une opération Release se synchronise avec (synchronizes-with) une opération Acquire sur la même variable. Ceci permet à l'écriture d'une variable dans un thread et à la lecture de cette variable dans un autre thread d'être cohérentes entre elles, sans donner plus de garanties sur l'ensemble du programme. Ces synchronisations peuvent être transitives, ce qui peut permettre par exemple d'utiliser des opérations d'écriture Relaxed jusqu'à un Release dans un thread, puis de précéder les lectures Relaxed d'une lecture Acquire dans un autre thread, et de synchroniser du même coup plusieurs lectures et écritures à coût réduit.

Le cas de memory_order_consume est particulier du fait qu'il ne suppose une dépendance Acquire-Release que sur la base des dépendances entre les données, qui surviennent quand une opération modifie A, qui est utilisé pour modifier B, qui sert ensuite pour modifier C, etc.. Sur certaines architectures, les opérations pour lesquelles les dépendances ne reposent que sur un tel enchaînement de modifications à des données sont naturelles, alors que d'autres opérations requièrent des modalités plus strictes (et sont par conséquent plus lentes à exécuter).

Relâché (Relaxed)

Une atomique relâchée est telle que les opérations qui s'appliquent sur elle sont indivises... et c'est tout. Il arrive que cette contrainte seule soit suffisante dans un programme, typiquement pour des raisons contextuelles, mais mieux vaut éviter de toucher à cette bestiole qui a pour impact concret dans un programme de faire en sorte que plusieurs threads d'un même programme voient les écritures aux variables partagées se faire potentiellement dans plus d'un ordre distinct.

En résumé avec un accès atomique relâché :

Sachant cela, pourquoi utiliserait-on des atomiques relâchées? Il y a quelques cas. Pensons par exemple à deux threads qui doivent incrémenter concurremment un même compteur : si notre seule préoccupation est que, suite à l'exécution des threads, la valeur du compteur soit cohérente, alors une incrémentation relâchée suffit. Notez toutefois qu'une atomique relâchée peut être plus rapide qu'une atomique séquentiellement cohérente, mais qu'elle ne le sera pas nécessairement, alors vérifiez vos hypothèses!

Implémenter un mutex simpliste avec lock(), try_lock() et unlock()

L'exemple qui suit est adapté d'un exemple d'un excellent article du brillant Jeff Preshing, article que vous trouverez sur http://preshing.com/20121019/this-is-why-they-call-it-a-weakly-ordered-cpu/ si vous souhaitez en savoir plus sur le sujet.

Implémentons un mutex « maison » à l'aide d'opérations atomiques. Notre mutex exposera les services de base attendus d'un tel type, soit lock(), try_lock() et unlock(). Une version de base serait :

#include <atomic>
class mini_mutex {
   std::atomic<int> fanion;
public:
   mini_mutex()
      : fanion{ 0 }
   {
   }
   bool try_lock() {
      int attendu = 0;
      return fanion.compare_exchange_strong(attendu, 1));
   }
   void lock() {
      int attendu = 0;
      while (!fanion.compare_exchange_weak(attendu, 1))
         ;
   }
   // precondition: le mutex est verrouillé
   void unlock() {
      fanion.store(0);
   }
};

Cette version utilise des opérations exigeant une pleine cohérence séquentielle, reposant sur le modèle opératoire par défaut qui est (implicitement) std::memory_order_seq_cst. Il se trouve qu'avec cette petite classe, nous n'avons pas besoin d'aussi fortes garanties, notre réel besoin étant d'assurer une synchronisation entre la prise du mutex et sa libération.

Une version opérationnellement équivalente mais potentiellement plus rapide car moins restrictive appliquerait des critères d'ordonnancement plus précis aux opérations sur l'attribut atomique fanion :

#include <atomic>
class mini_mutex {
   std::atomic<int> fanion;
public:
   mini_mutex()
      : fanion{ 0 }
   {
   }
   bool try_lock() {
      int attendu = 0;
      return fanion.compare_exchange_strong(attendu, 1, memory_order_acquire));
   }
   void lock() {
      int attendu = 0;
      while (!fanion.compare_exchange_weak(attendu, 1, memory_order_acquire))
         ;
   }
   // precondition: le mutex est verrouillé
   void unlock() {
      fanion.store(0, memory_order_release);
   }
};

Les opérations de type Acquire se synchronisent-avec les opérations de type Release sur une même variable.

Relations de synchronisation

On doit à Leslie Lamport la précieuse relation Happens-Before. Avec des atomiques séquentiellement cohérentes, Happens-Before sera respecté par défaut.

La relation Sequenced-Before décrit l'ordre « normal » d'exécution d'un programme dans une expression donnée : l'ordre des opérations dans le code source, essentiellement. Cette règle couvre des trucs tels que var = expr; expr sera évalué avant l'écriture dans val (sauf si un comportement indéfini en découle).

La relation Synchronizes-With a trait aux paires Release et Acquire sur une même variable, au sens où le Release fait précédemment par un thread se produira avant le Acquire correspondant sur un autre thread, du moins dans la mesure où les modèles de cohérence choisis le permettent.

La relation Carries-Dependency, surtout utile avec memory_order_consume, permet au code de respecter les dépendances reposant strictement sur l'ordre transitif de modification de données. On pourrait parler de Release-Consume. Si les compilateurs parviennent un jour à en tirer profit, memory_order_consume pourrait être un équivalent plus léger des paires memory_order_acquire et memory_order_release, mais au moment d'écrire ceci, nous n'en sommes pas encore là.

Une relation Dependency-Ordered-Before est une opération Acquire-Release qui ne dépend que de Carries-Dependency.

Une relation Inter-Thread-Happens-Before est faite d'une séquence transitive de Synchronizes-With et de Carries-Dependency, avec certaines subtilités quand à la nature des opérations impliquées.

La relation Happens-Before prend effet dans le cas d'un Sequenced-Before ou d'un Inter-Thread-Happens-Before.

Pointeurs intelligents atomiques

Avant de lire ce qui suit, mieux vaut être familière ou familier avec les pointeurs intelligents.

Ce dont nous discutons ci-dessous fait partie de la spécification technique sur la concurrence de C++ telle que prévue pour expérimentation, probablement en vue d'une inclusion dans le standard à partir de C++ 17.

Avec C++ 14, il est possible de faire un pointeur intelligent sur une atomique mais le standard n'offre pas de pointeur atomique intelligent. En vue de C++ 17, une paire des types clés envisagés est celle faite des types atomic_shared_ptr<T> et de atomic_weak_ptr<T>. Il semble qu'un atomic_unique_ptr<T> ne semble pas immédiatement pertinent, alors ce type est exclu des discussions pour le moment.

Outre les services habituels offerts par les versions non-atomiques de ces pointeurs intelligents, les services proposés de manière spécifique aux versions atomiques sont :

Évidemment, les divers services atomiques ont trait au pointeur, pas au pointé.

À l'aide de pointeurs intelligents atomiques, en particulier avec un atomic_shared_ptr<T>, il est possible d'éviter avec une certaine élégance le très vilain problème d'ABA susceptible de survenir sur des structures de données sans verrous. Herb Sutter en parle d'ailleurs dans https://www.youtube.com/watch?v=CmxkPChOcvw. Évidemment, on peut s'en sortir sans avoir accès à un tel pointeur intelligent, mais c'est beaucoup plus laborieux :

Avec un atomic_shared_ptr Sans un atomic_shared_ptr Notes
atomic_shared_ptr<T> asp;
shared_ptr<T> sp;

Bien entendu, sp n'est pas atomique, mais on peut manipuler ce vers quoi il pointe avec des opérations atomiques

auto p = asp.load();
auto p = atomic_load(&sp);

On peut appliquer une opération atomique sur l'adresse de sp comme on peut appliquer une opération (implicitement atomique) sur asp.

Il faut par contre le faire de manière volontaire, puisque ce n'est pas un automatisme sun un shared_ptr usuel.

asp.compare_exchange_weak(
   attendu, souhaite
);
atomic_compare_exchange_weak(
   &sp, attendu, souhaite
);

Lectures complémentaires

Herb Sutter a rendu disponible ses diapositives sur les atomiques à l'adresse https://onedrive.live.com/view.aspx?resid=4E86B0CF20EF15AD!24884&app=WordPdf&authkey=!AMtj_EflYn2507c

Aide en ligne sur les atomiques de C++ 11 :

Qu'est-ce qu'une opération atomique?

Ordonnancement en mémoire et réordonnancements des opérations :

Les opérations RCU (Read-Copy-Update), qui permettent de réaliser certaines actions synchronisées de manière extrêmement efficaces avec Linux, mais qui sont très difficiles à implémenter en termes de la sémantique d'un langage de programmation :

Série de textes très pédagogiques par le sympathique Jeff Preshing (les commentaires valent souvent la peine aussi, incluant des interventions d'experts tels que Bruce Dawson ou Herb Sutter) :

Dans les mots de l'auteur :

« Acquire semantics prevent memory reordering of the read-acquire with any read or write operation which follows it in program order » et « Release semantics prevent memory reordering of the write-release with any read or write operation which precedes it in program order »

Les exemples proposés montrent comment établir des relations sur la base de ces sémantiques avec des clôtures explicites, de même qu'avec des opérations sur des atomiques prises sur une base individuelle.

Selon l'auteur :

« An acquire fence prevents the memory reordering of any read which precedes it in program order with any read or write which follows it in program order » alors que « A release fence prevents the memory reordering of any read or write which precedes it in program order with any write which follows it in program order »

Décrit simplement, pour un programme strictement séquentiel et lu de haut en bas, une clôture en acquisition ne peut être déplacée vers le haut (tout ce qui la précède doit la précéder) alors qu'une clôture en libération ne peut être déplacée vers le bas (tout ce qui lui succède doit lui succéder).

Les conditions de course découlant d'une mauvaise compréhension de l'atomicité, par Larry Osterman en 2005 : http://blogs.msdn.com/b/larryosterman/archive/2005/02/11/371205.aspx

La question de l'atomicité est une question difficile, surtout face à la capacité qu'ont les compilateurs de réorganiser le code pour réaliser des optimisations. Plusieurs articles ont été écrits sur l'atomicité en fonction de C++ 11, plusieurs par Hans Boehm :

En Java, l'atomicité tend à être étudiée sous l'angle de la mémoire transactionnelle, mais le langage offre aussi des classes atomiques :

Valider les clôtures et l'atomicité dans le noyau de Linux, par Paul E. McKenney en 2011 : http://lwn.net/Articles/470681/

Concevoir des verrous à partir d'opérations atomiques, un texte de Steven Fuerst : http://locklessinc.com/articles/locks/

Réflexions sur les clôtures atomiques de C++ 11, par Charles Bloom en 2012 :

Atomicité et langage C (depuis C11) :

Atomicité et langages .NET :

Comparaison du code généré pour des calculs sur un entier, un entier volatile et un entier atomique, par Marc Brooker en 2013 : http://brooker.co.za/blog/2013/01/06/volatile.html

Combiner programmation sur le GPU et variables atomiques, par Elmar Westphal en 2015 : https://devblogs.nvidia.com/parallelforall/voting-and-shuffling-optimize-atomic-operations/

Exemples mettant de l'avant des appareils dont le modèle mémoire est faible :

Survol des modèles de cohérence de C++ 11, par Dan Maharry en 2012 : http://www.developerfusion.com/article/138018/memory-ordering-for-atomic-operations-in-c0x/

La programmation de structures synchronisées sans verrous, même si elle peut mener à des gains appréciables de rapidité, est une entreprise pour le moins périlleuse :

À propos des pointeurs intelligents atomiques :

Présentation d'Anthony Williams :


Valid XHTML 1.0 Transitional

CSS Valide !