variable - 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++?




variable automatique c++ (4)

C ++ 11 a introduit un modèle de mémoire standardisé, mais qu'est-ce que cela signifie exactement? Et comment cela va-t-il affecter la programmation C ++?

Cet article (par Gavin Clarke qui cite Herb Sutter ) dit que,

Le modèle de mémoire signifie que le code C ++ a désormais une bibliothèque standardisée à appeler, quel que soit l'auteur du compilateur et sur quelle plate-forme il s'exécute. Il existe un moyen standard de contrôler la façon dont les différents threads communiquent avec la mémoire du processeur.

« Quand vous parlez de partage [code] entre les différents noyaux qui sont dans la norme, nous parlons du modèle de mémoire. Nous allons l' optimiser sans casser les hypothèses suivantes les gens vont faire dans le code », a déclaré Sutter.

Eh bien, je peux mémoriser ceci et des paragraphes similaires disponibles en ligne (comme j'ai eu mon propre modèle de mémoire depuis la naissance: P) et je peux même poster comme réponse aux questions posées par d'autres, mais pour être honnête, je ne comprends pas .

Donc, ce que je veux essentiellement savoir, c'est que les programmeurs C ++ avaient l'habitude de développer des applications multithread avant même, alors qu'importe si c'est des threads POSIX, ou des threads Windows, ou des threads C ++ 11? Quels sont les bénéfices? Je veux comprendre les détails de bas niveau.

J'ai également l'impression que le modèle de mémoire C ++ 11 est en quelque sorte lié au support multi-threading C ++ 11, car je vois souvent ces deux ensemble. Si c'est le cas, comment exactement? Pourquoi devraient-ils être liés?

Comme je ne sais pas comment fonctionne le fonctionnement du multi-threading, et ce que signifie le modèle de mémoire en général, aidez-moi à comprendre ces concepts. :-)


Cela signifie que la norme définit désormais le multithread et définit ce qui se passe dans le contexte de plusieurs threads. Bien sûr, les gens utilisaient des implémentations différentes, mais c'est comme demander pourquoi nous devrions avoir une std::string quand nous pourrions tous utiliser une classe de string .

Quand vous parlez de threads POSIX ou de threads Windows, alors c'est un peu une illusion car vous parlez de threads x86, car c'est une fonction matérielle à exécuter simultanément. Le modèle de mémoire C ++ 0x apporte des garanties, que vous soyez sur x86, ou ARM, ou MIPS , ou toute autre chose que vous pouvez imaginer.


D'abord, vous devez apprendre à penser comme un avocat de la langue.

La spécification C ++ ne fait référence à aucun compilateur, système d'exploitation ou processeur particulier. Il fait référence à une machine abstraite qui est une généralisation de systèmes réels. Dans le monde de l'avocat des langues, le travail du programmeur consiste à écrire du code pour la machine abstraite; le travail du compilateur consiste à actualiser ce code sur une machine concrète. En codant de manière rigide à la spécification, vous pouvez être certain que votre code sera compilé et exécuté sans modification sur n'importe quel système avec un compilateur C ++ conforme, que ce soit aujourd'hui ou dans 50 ans.

La machine abstraite dans la spécification C ++ 98 / C ++ 03 est fondamentalement mono-thread. Il n'est donc pas possible d'écrire du code C ++ multithread qui soit "entièrement portable" par rapport à la spécification. La spécification ne parle même pas de l' atomicité des charges de mémoire et des magasins ou de l' ordre dans lequel les chargements et les magasins peuvent se produire, sans parler des choses comme les mutex.

Bien sûr, vous pouvez écrire du code multithread dans la pratique pour des systèmes concrets particuliers - comme pthreads ou Windows. Mais il n'existe aucun moyen standard d'écrire du code multithread pour C ++ 98 / C ++ 03.

La machine abstraite dans C ++ 11 est multi-thread par conception. Il a également un modèle de mémoire bien défini; c'est à dire, ce que le compilateur peut faire et ne pas faire quand il s'agit d'accéder à la mémoire.

Considérons l'exemple suivant, où une paire de variables globales est accédée simultanément par deux threads:

           Global
           int x, y;

Thread 1            Thread 2
x = 17;             cout << y << " ";
y = 37;             cout << x << endl;

Que pourrait Thread 2 sortie?

Sous C ++ 98 / C ++ 03, ce comportement n'est même pas indéfini; la question elle-même n'a pas de sens parce que la norme n'envisage rien de ce qu'on appelle un «fil».

Sous C ++ 11, le résultat est Undefined Behavior, car les charges et les magasins n'ont pas besoin d'être atomiques en général. Ce qui peut ne pas sembler être une amélioration ... Et en soi, ce n'est pas le cas.

Mais avec C ++ 11, vous pouvez écrire ceci:

           Global
           atomic<int> x, y;

Thread 1                 Thread 2
x.store(17);             cout << y.load() << " ";
y.store(37);             cout << x.load() << endl;

Maintenant, les choses deviennent beaucoup plus intéressantes. Tout d'abord, le comportement ici est défini . Thread 2 peut maintenant imprimer 0 0 (s'il s'exécute avant Thread 1), 37 17 (s'il s'exécute après Thread 1) ou 0 17 (s'il s'exécute après que Thread 1 assigne à x mais avant qu'il ne l'affecte à y).

Ce qu'il ne peut pas imprimer est 37 0 , car le mode par défaut pour les charges / magasins atomiques dans C ++ 11 consiste à appliquer la cohérence séquentielle . Cela signifie simplement que toutes les charges et tous les magasins doivent être «comme si» ils se produisaient dans l'ordre où vous les avez écrits dans chaque thread, tandis que les opérations entre les threads peuvent être entrelacées, quel que soit le système. Ainsi, le comportement par défaut des atomes fournit à la fois l' atomicité et l' ordre pour les charges et les magasins.

Maintenant, sur un processeur moderne, assurer une cohérence séquentielle peut être coûteux. En particulier, le compilateur est susceptible d'émettre des barrières de mémoire à part entière entre chaque accès ici. Mais si votre algorithme peut tolérer des chargements et des stocks en désordre; c'est-à-dire, si cela requiert l'atomicité mais pas l'ordre; c'est-à-dire, s'il peut tolérer 37 0 en sortie de ce programme, alors vous pouvez écrire ceci:

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_relaxed);   cout << y.load(memory_order_relaxed) << " ";
y.store(37,memory_order_relaxed);   cout << x.load(memory_order_relaxed) << endl;

Plus le processeur est moderne, plus il est probable qu'il soit plus rapide que l'exemple précédent.

Enfin, si vous avez juste besoin de garder des charges particulières et des magasins en ordre, vous pouvez écrire:

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_release);   cout << y.load(memory_order_acquire) << " ";
y.store(37,memory_order_release);   cout << x.load(memory_order_acquire) << endl;

Cela nous ramène aux charges et aux magasins commandés - 37 0 n'est donc plus une sortie possible - mais il le fait avec un minimum de frais généraux. (Dans cet exemple trivial, le résultat est le même que la cohérence séquentielle complète, dans un programme plus grand, ce ne serait pas le cas.)

Bien sûr, si les seules sorties que vous voulez voir sont 0 0 ou 37 17 , vous pouvez juste enrouler un mutex autour du code original. Mais si vous avez lu jusqu'ici, je parie que vous savez déjà comment cela fonctionne, et cette réponse est déjà plus longue que prévu :-).

Donc, la ligne de fond. Les mutex sont excellents, et C ++ 11 les standardise. Mais parfois, pour des raisons de performance, vous voulez des primitives de niveau inférieur (par exemple, le schéma de verrouillage classique à double vérification ). La nouvelle norme fournit des gadgets de haut niveau tels que les mutex et les variables de condition, et fournit également des gadgets de bas niveau tels que les types atomiques et les différentes variantes de la barrière de mémoire. Vous pouvez maintenant écrire des routines simultanées sophistiquées et performantes entièrement dans la langue spécifiée par la norme, et vous pouvez être certain que votre code sera compilé et fonctionnera sans changement sur les systèmes d'aujourd'hui et de demain.

Bien que, pour être franc, à moins d'être un expert et de travailler sur un code sérieux de bas niveau, vous devriez probablement vous en tenir aux mutex et aux variables de condition. C'est ce que j'ai l'intention de faire.

Pour plus d'informations sur ce sujet, consultez cet article de blog .


Pour les langues qui ne spécifient pas de modèle de mémoire, vous écrivez du code pour la langue et le modèle de mémoire spécifiés par l'architecture du processeur. Le processeur peut choisir de réorganiser les accès mémoire pour des performances. Donc, si votre programme a des courses de données (une course de données est quand il est possible que plusieurs cœurs / hyper-threads accèdent simultanément à la même mémoire) alors votre programme n'est pas multiplateforme en raison de sa dépendance au modèle de mémoire du processeur. Vous pouvez vous reporter aux manuels des logiciels Intel ou AMD pour savoir comment les processeurs peuvent réorganiser les accès mémoire.

Très important, les verrous (et la sémantique de concurrence avec verrouillage) sont généralement implémentés de manière multi-plateforme ... Donc, si vous utilisez des verrous standard dans un programme multithread sans courses de données, vous n'avez pas à vous soucier des modèles de mémoire multiplateformes .

Il est intéressant de noter que les compilateurs Microsoft pour C ++ ont une sémantique d'acquisition / libération de volatile qui est une extension C ++ pour faire face à l'absence d'un modèle de mémoire en C ++ http://msdn.microsoft.com/en-us/library/12a04hfd(v=vs.80).aspx . Cependant, étant donné que Windows s'exécute uniquement sur x86 / x64, cela ne signifie pas grand-chose (les modèles de mémoire Intel et AMD facilitent la mise en œuvre d'une sémantique d'acquisition / libération dans une langue).


Si vous utilisez des mutex pour protéger toutes vos données, vous ne devriez vraiment pas avoir à vous inquiéter. Les mutex ont toujours fourni des garanties de commande et de visibilité suffisantes.

Maintenant, si vous avez utilisé des algorithmes atomiques ou sans verrou, vous devez penser au modèle de mémoire. Le modèle de mémoire décrit précisément quand les atomiques fournissent des garanties de commande et de visibilité, et fournit des clôtures portables pour les garanties codées à la main.

Auparavant, atomics serait fait en utilisant des intrinsèques du compilateur, ou une bibliothèque de niveau supérieur. Les clôtures auraient été faites en utilisant des instructions spécifiques au CPU (barrières de mémoire).





memory-model