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

1894

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 ++ dispose désormais d'une bibliothèque normalisée à appeler, peu importe qui a créé le compilateur et sur quelle plate-forme il fonctionne. Il existe un moyen standard de contrôler la façon dont les différents threads communiquent avec la mémoire du processeur.

"Lorsque vous parlez de diviser [le code] entre différents cœurs qui sont dans la norme, nous parlons du modèle de mémoire. Nous allons l'optimiser sans casser les hypothèses suivantes que les gens vont faire dans le code", a déclaré Sutter .

Eh bien, je peux mémoriser cela et des paragraphes similaires disponibles en ligne (car j'ai 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 exactement cette.

Les programmeurs C ++ avaient l'habitude de développer des applications multithread même avant, alors quelle importance si ce sont des threads POSIX, ou des threads Windows ou 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é à la prise en charge multithread C ++ 11, comme 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 fonctionnent les fonctions internes du multi-threading et ce que signifie le modèle de mémoire en général, veuillez m'aider à comprendre ces concepts. :-)

Nawaz
la source
3
@curiousguy: Elaborate ...
Nawaz
4
@curiousguy: Écrivez un blog alors ... et proposez également un correctif. Il n'y a pas d'autre moyen de valider votre argumentation et sa justification.
Nawaz
2
J'ai pris ce site pour un endroit où poser des questions et échanger des idées. Ma faute; c'est un lieu de conformité où vous ne pouvez pas être en désaccord avec Herb Sutter même quand il se contredit de manière flagrante au sujet de la spécification de lancer.
curiousguy
5
@curiousguy: C ++ est ce que dit la norme, pas ce que dit un gars au hasard sur Internet. Alors oui, il doit y avoir conformité avec la norme. C ++ n'est PAS une philosophie ouverte où vous pouvez parler de tout ce qui n'est pas conforme à la norme.
Nawaz
3
"J'ai prouvé qu'aucun programme C ++ ne peut avoir un comportement bien défini." . De grandes réclamations, sans aucune preuve!
Nawaz

Réponses:

2205

Tout d'abord, vous devez apprendre à penser comme un avocat spécialisé en langues.

La spécification C ++ ne fait référence à aucun compilateur, système d'exploitation ou CPU particulier. Il fait référence à une machine abstraite qui est une généralisation de systèmes réels. Dans le monde du Language Lawyer, le travail du programmeur est d'écrire du code pour la machine abstraite; le travail du compilateur est d'actualiser ce code sur une machine à béton. En codant de manière rigide selon les spécifications, vous pouvez être certain que votre code se compilera et s'exécutera sans modification sur n'importe quel système doté d'un compilateur C ++ conforme, que ce soit aujourd'hui ou dans 50 ans.

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

Bien sûr, vous pouvez écrire du code multi-thread 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 en C ++ 11 est multithread par conception. Il a également un modèle de mémoire bien défini ; c'est-à-dire qu'il indique ce que le compilateur peut et ne peut pas faire lorsqu'il s'agit d'accéder à la mémoire.

Prenons l'exemple suivant, où une paire de variables globales est accessible simultanément par deux threads:

           Global
           int x, y;

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

Que pourrait produire la sortie de Thread 2?

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

Sous C ++ 11, le résultat est un comportement indéfini, car les chargements et les magasins n'ont pas besoin d'être atomiques en général. Ce qui peut ne pas sembler être une grande 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 est ici défini . Le thread 2 peut désormais s'imprimer 0 0(s'il s'exécute avant le thread 1), 37 17(s'il s'exécute après le thread 1) ou 0 17(s'il s'exécute après que le thread 1 a été affecté à x mais avant qu'il soit affecté à y).

Ce qu'il ne peut pas imprimer 37 0, c'est parce que le mode par défaut pour les chargements / magasins atomiques en C ++ 11 est d'appliquer la cohérence séquentielle . Cela signifie simplement que toutes les charges et tous les magasins doivent être "comme si" ils se sont produits dans l'ordre dans lequel vous les avez écrits dans chaque thread, tandis que les opérations entre les threads peuvent être entrelacées comme le système le souhaite. Ainsi, le comportement par défaut de l'atomique fournit à la fois l' atomicité et l' ordre des charges et des magasins.

Désormais, sur un processeur moderne, assurer la 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 magasins hors service; c'est-à-dire si elle nécessite une atomicité mais pas de commande; c'est-à-dire, s'il peut tolérer 37 0comme 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 et des magasins particuliers dans l'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 - ce 37 0n'est donc plus une sortie possible - mais cela 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 à part entière; dans un programme plus grand, il ne le serait pas.)

Bien sûr, si les seules sorties que vous souhaitez voir sont 0 0ou 37 17, vous pouvez simplement enrouler un mutex autour du code d'origine. 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 je ne le pensais :-).

Donc, en bout de ligne. Les mutex sont excellents et C ++ 11 les standardise. Mais parfois, pour des raisons de performances, vous souhaitez des primitives de niveau inférieur (par exemple, le schéma de verrouillage classique à double vérification ). Le nouveau standard fournit des gadgets de haut niveau comme les mutex et les variables de condition, et il fournit également des gadgets de bas niveau comme les types atomiques et les différentes saveurs de la barrière de mémoire. Vous pouvez donc maintenant écrire des routines simultanées sophistiquées et hautes performances entièrement dans le langage spécifié par la norme, et vous pouvez être certain que votre code se compilera et s'exécutera inchangé sur les systèmes d'aujourd'hui et de demain.

Bien que pour être franc, à moins que vous ne soyez un expert et que vous travailliez sur un code de bas niveau sérieux, vous devriez probablement vous en tenir aux mutex et aux variables de condition. Voilà ce que je compte faire.

Pour en savoir plus sur ce sujet, consultez cet article de blog .

Nemo
la source
37
Belle réponse, mais cela demande vraiment quelques exemples réels des nouvelles primitives. En outre, je pense que l'ordre de la mémoire sans primitives est le même que celui du pré-C ++ 0x: il n'y a aucune garantie.
John Ripley
5
@John: Je sais, mais j'apprends toujours les primitives moi-même :-). De plus, je pense qu'ils garantissent que les octets sont atomiques (bien qu'ils ne soient pas commandés), c'est pourquoi je suis allé avec "char" pour mon exemple ... Mais je ne suis même pas sûr à 100% de cela ... Si vous voulez suggérer quelque chose de bon " tutoriel "références je les ajouterai à ma réponse
Nemo
48
@Nawaz: Oui! Les accès à la mémoire peuvent être réorganisés par le compilateur ou le CPU. Pensez (par exemple) aux caches et aux charges spéculatives. L'ordre dans lequel la mémoire système est touchée ne ressemble en rien à ce que vous avez codé. Le compilateur et le processeur garantissent que ces réorganisations ne cassent pas le code à thread unique . Pour le code multithread, le "modèle de mémoire" caractérise les réordonnancements possibles, et ce qui se passe si deux threads lisent / écrivent le même emplacement en même temps, et comment vous exercez un contrôle sur les deux. Pour le code à thread unique, le modèle de mémoire n'est pas pertinent.
Nemo
26
@Nawaz, @Nemo - Un détail mineur: le nouveau modèle de mémoire est pertinent dans le code à thread unique dans la mesure où il spécifie le caractère indéfini de certaines expressions, telles que i = i++. L'ancien concept de points de séquence a été abandonné; la nouvelle norme spécifie la même chose en utilisant une relation séquencée avant qui n'est qu'un cas particulier du concept plus général inter-thread passe avant .
JohannesD
17
@ AJG85: la section 3.6.2 du projet de spécification C ++ 0x dit: "Les variables avec une durée de stockage statique (3.7.1) ou une durée de stockage de thread (3.7.2) doivent être initialisées à zéro (8.5) avant toute autre initialisation. endroit." Étant donné que x, y sont globaux dans cet exemple, ils ont une durée de stockage statique et seront donc initialisés à zéro, je crois.
Nemo
345

Je vais juste donner l'analogie avec laquelle je comprends les modèles de cohérence de la mémoire (ou les modèles de mémoire, pour faire court). Il est inspiré par le papier séminal de Leslie Lamport "Time, Clocks, and the Ordering of Events in a Distributed System" . L'analogie est appropriée et a une signification fondamentale, mais peut être excessive pour de nombreuses personnes. Cependant, j'espère qu'il fournit une image mentale (une représentation picturale) qui facilite le raisonnement sur les modèles de cohérence de la mémoire.

Voyons les historiques de tous les emplacements de mémoire dans un diagramme espace-temps dans lequel l'axe horizontal représente l'espace d'adressage (c'est-à-dire que chaque emplacement de mémoire est représenté par un point sur cet axe) et l'axe vertical représente le temps (nous verrons que, en général, il n'y a pas de notion universelle du temps). L'historique des valeurs détenues par chaque emplacement mémoire est donc représenté par une colonne verticale à cette adresse mémoire. Chaque changement de valeur est dû à l'un des threads écrivant une nouvelle valeur à cet emplacement. Par une image de mémoire , nous entendrons l'ensemble / combinaison de valeurs de tous les emplacements de mémoire observables à un moment donné par un thread particulier .

Citation de "Une introduction sur la cohérence de la mémoire et la cohérence du cache"

Le modèle de mémoire intuitif (et le plus restrictif) est la cohérence séquentielle (SC) dans laquelle une exécution multithread devrait ressembler à un entrelacement des exécutions séquentielles de chaque thread constituant, comme si les threads étaient multiplexés dans le temps sur un processeur monocœur.

Cet ordre de mémoire globale peut varier d'une exécution du programme à l'autre et peut ne pas être connu à l'avance. La caractéristique de SC est l'ensemble des tranches horizontales dans le diagramme espace-temps-adresse représentant des plans de simultanéité (c'est-à-dire des images de mémoire). Sur un plan donné, tous ses événements (ou valeurs de mémoire) sont simultanés. Il existe une notion de temps absolu , dans laquelle tous les threads s'accordent sur les valeurs de mémoire qui sont simultanées. Dans SC, à chaque instant, il n'y a qu'une seule image mémoire partagée par tous les threads. Autrement dit, à chaque instant, tous les processeurs s'accordent sur l'image de la mémoire (c'est-à-dire le contenu agrégé de la mémoire). Cela implique non seulement que tous les threads affichent la même séquence de valeurs pour tous les emplacements de mémoire, mais également que tous les processeurs observent la mêmecombinaisons de valeurs de toutes les variables. Cela revient à dire que toutes les opérations de mémoire (sur tous les emplacements de mémoire) sont observées dans le même ordre total par tous les threads.

Dans les modèles de mémoire détendue, chaque thread divisera l'espace adresse-temps à sa manière, la seule restriction étant que les tranches de chaque thread ne doivent pas se croiser car tous les threads doivent s'accorder sur l'historique de chaque emplacement de mémoire individuel (bien sûr , des tranches de fils différents peuvent et vont se croiser). Il n'y a pas de moyen universel de le découper (pas de foliation privilégiée de l'espace-temps-adresse). Les tranches n'ont pas besoin d'être planes (ou linéaires). Ils peuvent être incurvés et c'est ce qui peut faire qu'un thread lit des valeurs écrites par un autre thread dans l'ordre dans lequel elles ont été écrites. Les histoires de différents emplacements mémoire peuvent glisser (ou s'étirer) arbitrairement les unes par rapport aux autres lorsqu'elles sont vues par un thread particulier. Chaque thread aura un sens différent des événements (ou, de manière équivalente, des valeurs de mémoire) qui sont simultanés. L'ensemble des événements (ou valeurs de mémoire) qui sont simultanés à un thread ne sont pas simultanés à un autre. Ainsi, dans un modèle de mémoire détendue, tous les threads observent toujours la même histoire (c'est-à-dire la séquence de valeurs) pour chaque emplacement de mémoire. Mais ils peuvent observer des images de mémoire différentes (c'est-à-dire des combinaisons de valeurs de tous les emplacements de mémoire). Même si deux emplacements de mémoire différents sont écrits par le même thread en séquence, les deux nouvelles valeurs écrites peuvent être observées dans un ordre différent par d'autres threads.

[Image de Wikipedia] Image de Wikipedia

Les lecteurs familiers avec la théorie spéciale de la relativité d'Einstein remarqueront ce à quoi je fais allusion. Traduire les mots de Minkowski dans le domaine des modèles de mémoire: l'espace adresse et le temps sont des ombres de l'espace adresse temps. Dans ce cas, chaque observateur (c.-à-d., Fil) projettera des ombres d'événements (c.-à-d. Des mémoires / charges de mémoire) sur sa propre ligne du monde (c.-à-d., Son axe temporel) et son propre plan de simultanéité (son axe d'espace d'adressage) . Les threads du modèle de mémoire C ++ 11 correspondent à des observateurs qui se déplacent les uns par rapport aux autres en relativité restreinte. La cohérence séquentielle correspond à l' espace-temps galiléen (c'est -à- dire que tous les observateurs s'accordent sur un ordre absolu d'événements et un sens global de simultanéité).

La ressemblance entre les modèles de mémoire et la relativité restreinte provient du fait que les deux définissent un ensemble d'événements partiellement ordonné, souvent appelé un ensemble causal. Certains événements (c'est-à-dire les mémoires de stockage) peuvent affecter (mais ne pas être affectés par) d'autres événements. Un thread C ++ 11 (ou observateur en physique) n'est rien de plus qu'une chaîne (c'est-à-dire un ensemble totalement ordonné) d'événements (par exemple, la mémoire charge et stocke à des adresses éventuellement différentes).

En relativité, un certain ordre est rétabli à l'image apparemment chaotique d'événements partiellement ordonnés, puisque le seul ordre temporel sur lequel tous les observateurs s'accordent est l'ordre parmi les événements «temporels» (c'est-à-dire, ces événements qui sont en principe connectables par n'importe quelle particule qui ralentit). que la vitesse de la lumière dans le vide). Seuls les événements liés au temps sont ordonnés de manière invariante. Temps en physique, Craig Callender .

Dans le modèle de mémoire C ++ 11, un mécanisme similaire (le modèle de cohérence acquisition-libération) est utilisé pour établir ces relations de causalité locales .

Pour fournir une définition de la cohérence de la mémoire et une motivation pour abandonner SC, je citerai "Une introduction sur la cohérence de la mémoire et la cohérence du cache"

Pour une machine à mémoire partagée, le modèle de cohérence de la mémoire définit le comportement architecturalement visible de son système de mémoire. Le critère d'exactitude pour un comportement de partitionnement d'un cœur de processeur unique entre « un résultat correct » et «de nombreuses alternatives incorrectes ». Cela est dû au fait que l'architecture du processeur exige que l'exécution d'un thread transforme un état d'entrée donné en un seul état de sortie bien défini, même sur un cœur hors service. Cependant, les modèles de cohérence de la mémoire partagée concernent les charges et les magasins de plusieurs threads et permettent généralement de nombreuses exécutions correctestout en interdisant de nombreuses (plus) fausses. La possibilité de plusieurs exécutions correctes est due au fait que l'ISA permet à plusieurs threads de s'exécuter simultanément, souvent avec de nombreux entrelacements juridiques possibles d'instructions provenant de différents threads.

Les modèles de cohérence de la mémoire détendue ou faible sont motivés par le fait que la plupart des commandes de mémoire dans les modèles forts ne sont pas nécessaires. Si un thread met à jour dix éléments de données puis un indicateur de synchronisation, les programmeurs ne se soucient généralement pas si les éléments de données sont mis à jour l'un par rapport à l'autre, mais seulement que tous les éléments de données sont mis à jour avant la mise à jour de l'indicateur (généralement implémenté à l'aide des instructions FENCE ). Les modèles détendus cherchent à capter cette flexibilité accrue de commande et à ne conserver que les commandes dont les programmeurs «ont besoin ».”Pour obtenir à la fois des performances plus élevées et l'exactitude de SC. Par exemple, dans certaines architectures, les tampons d'écriture FIFO sont utilisés par chaque cœur pour conserver les résultats des magasins validés (retirés) avant d'écrire les résultats dans les caches. Cette optimisation améliore les performances mais viole SC. Le tampon d'écriture masque la latence du traitement d'un échec de magasin. Parce que les magasins sont courants, être en mesure d'éviter de caler sur la plupart d'entre eux est un avantage important. Pour un processeur simple cœur, un tampon d'écriture peut être rendu architecturalement invisible en garantissant qu'une charge à l'adresse A renvoie la valeur du magasin le plus récent à A même si un ou plusieurs magasins à A sont dans le tampon d'écriture. Cela se fait généralement en contournant la valeur du magasin le plus récent vers A à la charge de A, où «le plus récent» est déterminé par l'ordre du programme, ou en bloquant une charge de A si un magasin vers A est dans le tampon d'écriture. Lorsque plusieurs cœurs sont utilisés, chacun aura son propre tampon d'écriture de contournement. Sans tampons d'écriture, le matériel est SC, mais pas avec les tampons d'écriture, ce qui rend les tampons d'écriture visibles sur le plan architectural dans un processeur multicœur.

La réorganisation de magasin à magasin peut se produire si un cœur a un tampon d'écriture non FIFO qui permet aux magasins de s'écarter dans un ordre différent de celui dans lequel ils sont entrés. Cela peut se produire si le premier magasin manque dans le cache pendant que le second frappe ou si le deuxième magasin peut fusionner avec un magasin précédent (c'est-à-dire avant le premier magasin). Un réordonnancement de charge-charge peut également se produire sur des cœurs planifiés dynamiquement qui exécutent des instructions hors de l'ordre du programme. Cela peut se comporter comme la réorganisation des magasins sur un autre noyau (pouvez-vous trouver un exemple d'entrelacement entre deux threads?). Réorganiser une charge antérieure avec un magasin ultérieur (une réorganisation de magasin de chargement) peut provoquer de nombreux comportements incorrects, tels que le chargement d'une valeur après avoir relâché le verrou qui la protège (si le magasin est l'opération de déverrouillage).

Parce que la cohérence du cache et la cohérence de la mémoire sont parfois confondues, il est instructif d'avoir également cette citation:

Contrairement à la cohérence, la cohérence du cache n'est ni visible ni requise par le logiciel. La cohérence vise à rendre les caches d'un système à mémoire partagée aussi fonctionnellement invisibles que les caches d'un système monocœur. Une cohérence correcte garantit qu'un programmeur ne peut pas déterminer si et où un système a des caches en analysant les résultats des charges et des magasins. En effet, une cohérence correcte garantit que les caches n'activent jamais un comportement fonctionnel nouveau ou différent (les programmeurs peuvent toujours être en mesure d'inférer la structure de cache probable à l'aide de la synchronisationinformation). L'objectif principal des protocoles de cohérence de cache est de maintenir l'invariant SWMR (single-writer-multiple-reader) pour chaque emplacement de mémoire. Une distinction importante entre cohérence et cohérence est que la cohérence est spécifiée sur une base d'emplacement par mémoire , tandis que la cohérence est spécifiée par rapport à tous les emplacements de mémoire.

Poursuivant notre image mentale, l'invariant SWMR correspond à l'exigence physique qu'il doit y avoir au plus une particule située à n'importe quel endroit mais il peut y avoir un nombre illimité d'observateurs de n'importe quel endroit.

Ahmed Nassar
la source
52
+1 pour l'analogie avec la relativité restreinte, j'ai essayé de faire la même analogie moi-même. Trop souvent, je vois des programmeurs enquêter sur du code threadé essayant d'interpréter le comportement comme des opérations dans différents threads se produisant entrelacées les unes dans les autres dans un ordre spécifique, et je dois leur dire, non, avec les systèmes multiprocesseurs, la notion de simultanéité entre différents <s > les cadres de référence </s> n'ont plus de sens. La comparaison avec la relativité restreinte est un bon moyen de les faire respecter la complexité du problème.
Pierre Lebeaupin
71
Alors devriez-vous conclure que l'Univers est multicœur?
Peter K
6
@PeterK: Exactement :) Et voici une très belle visualisation de cette image du temps par le physicien Brian Greene: youtube.com/watch?v=4BjGWLJNPcA&t=22m12s Ceci est "L'illusion du temps [documentaire complet]" à la minute 22 et 12 secondes.
Ahmed Nassar
2
Est-ce juste moi ou passe-t-il d'un modèle de mémoire 1D (axe horizontal) à un modèle de mémoire 2D (plans de simultanéité). Je trouve cela un peu déroutant mais c'est peut-être parce que je ne suis pas natif ... Encore une lecture très intéressante.
Au revoir SE
Vous avez oublié une partie essentielle: " en analysant les résultats des chargements et des magasins " ... sans utiliser des informations de chronométrage précises.
curiousguy
115

C'est maintenant une question de plusieurs années, mais étant très populaire, il convient de mentionner une ressource fantastique pour en savoir plus sur le modèle de mémoire C ++ 11. Je ne vois pas l'intérêt de résumer son discours afin de faire une autre réponse complète, mais étant donné que c'est le gars qui a effectivement écrit la norme, je pense que cela vaut la peine de regarder le discours.

Herb Sutter a une longue conversation de trois heures sur le modèle de mémoire C ++ 11 intitulé "Atomic <> Weapons", disponible sur le site Channel9 - partie 1 et partie 2 . La conférence est assez technique et couvre les sujets suivants:

  1. Optimisations, races et modèle de mémoire
  2. Commande - Quoi: Acquérir et libérer
  3. Commande - Comment: Mutex, Atomics et / ou Clôtures
  4. Autres restrictions sur les compilateurs et le matériel
  5. Génération et performances du code: x86 / x64, IA64, POWER, ARM
  6. Atomics détendus

L'exposé ne traite pas de l'API, mais plutôt du raisonnement, du contexte, sous le capot et dans les coulisses (saviez-vous que la sémantique détendue a été ajoutée à la norme uniquement parce que POWER et ARM ne prennent pas en charge efficacement la charge synchronisée?).

eran
la source
10
Cette conversation est en effet fantastique, elle vaut totalement les 3 heures que vous passerez à la regarder.
ZunTzu
5
@ZunTzu: sur la plupart des lecteurs vidéo, vous pouvez régler la vitesse sur 1,25, 1,5 ou même 2 fois l'original.
Christian Severin
4
@eran avez-vous des photos des diapositives? les liens sur les pages de discussion du canal 9 ne fonctionnent pas.
athos
2
@athos Je ne les ai pas, désolé. Essayez de contacter le canal 9, je ne pense pas que la suppression était intentionnelle (je suppose qu'ils ont obtenu le lien de Herb Sutter, publié tel quel, et il a ensuite supprimé les fichiers; mais ce n'est qu'une spéculation ...).
eran
75

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 ont utilisé différentes implémentations, mais c'est comme demander pourquoi nous devrions avoir un std::stringalors que nous pourrions tous utiliser une stringclasse maison.

Lorsque vous parlez de threads POSIX ou de threads Windows, c'est un peu une illusion car en réalité vous parlez de threads x86, car c'est une fonction matérielle à exécuter simultanément. Le modèle de mémoire C ++ 0x offre des garanties, que vous soyez sur x86, ARM ou MIPS , ou tout ce que vous pouvez trouver.

Chiot
la source
28
Les threads Posix ne sont pas limités à x86. En effet, les premiers systèmes sur lesquels ils ont été implémentés n'étaient probablement pas des systèmes x86. Les threads Posix sont indépendants du système et sont valables sur toutes les plateformes Posix. Ce n'est pas vraiment vrai non plus qu'il s'agit d'une propriété matérielle car les threads Posix peuvent également être implémentés via le multitâche coopératif. Mais bien sûr, la plupart des problèmes de threading n'apparaissent que sur les implémentations de threading matériel (et certains même uniquement sur les systèmes multiprocesseurs / multicœurs).
celtschk
57

Pour les langues ne spécifiant 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 à la mémoire pour des performances. Donc, si votre programme a des courses de données (une course de données, c'est quand il est possible pour plusieurs cœurs / hyper-threads d'accéder simultanément à la même mémoire), alors votre programme n'est pas multiplateforme en raison de sa dépendance sur le modèle de mémoire du processeur. Vous pouvez vous référer aux manuels des logiciels Intel ou AMD pour savoir comment les processeurs peuvent réorganiser les accès à la 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 multiplateforme ... Donc, si vous utilisez des verrous standard dans un programme multithread sans course de données, vous n'avez pas à vous soucier des modèles de mémoire multiplateforme .

Fait intéressant, les compilateurs Microsoft pour C ++ ont acquis / publié une sémantique pour 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 fonctionne uniquement sur x86 / x64, cela ne veut pas dire grand-chose (les modèles de mémoire Intel et AMD facilitent et mettent en œuvre efficacement la sémantique d'acquisition / publication dans un langage).

ritesh
la source
2
Il est vrai que, lorsque la réponse a été écrite, Windows s'exécutait uniquement sur x86 / x64, mais Windows fonctionnait, à un moment donné, sur IA64, MIPS, Alpha AXP64, PowerPC et ARM. Aujourd'hui, il fonctionne sur différentes versions d'ARM, qui est assez différente en termes de mémoire de x86, et nulle part aussi indulgente.
Lorenzo Dematté
Ce lien est quelque peu rompu (indique la «documentation retirée de Visual Studio 2005» ). Voulez-vous le mettre à jour?
Peter Mortensen
3
Ce n'était pas vrai même lorsque la réponse a été écrite.
Ben
« Pour accéder à la même mémoire en même temps que » l'accès à un conflit moyen
curiousguy
27

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 verrouillage, vous devez penser au modèle de mémoire. Le modèle de mémoire décrit précisément quand l'atomique fournit des garanties de commande et de visibilité, et fournit des clôtures portables pour des garanties codées à la main.

Auparavant, l'atomique était effectuée à l'aide de l'intrinsèque du compilateur ou d'une bibliothèque de niveau supérieur. Les clôtures auraient été réalisées à l'aide d'instructions spécifiques au processeur (barrières de mémoire).

ninjalj
la source
19
Le problème avant était qu'il n'existait pas de mutex (en termes de standard C ++). Ainsi, les seules garanties qui vous ont été fournies étaient par le fabricant du mutex, ce qui était bien tant que vous ne portiez pas le code (car les modifications mineures des garanties sont difficiles à repérer). Maintenant, nous obtenons des garanties fournies par la norme qui devrait être portable entre les plates-formes.
Martin York
4
@Martin: dans tous les cas, une chose est le modèle de mémoire, et une autre sont les primitives atomiques et de threading qui s'exécutent au-dessus de ce modèle de mémoire.
ninjalj
4
En outre, mon point de vue était principalement que, auparavant, il n'y avait généralement pas de modèle de mémoire au niveau du langage, il s'agissait du modèle de mémoire du processeur sous-jacent. Maintenant, il existe un modèle de mémoire qui fait partie du langage de base; OTOH, les mutex et similaires pourraient toujours être réalisés comme une bibliothèque.
ninjalj
3
Cela pourrait aussi être un vrai problème pour les gens qui essaient d' écrire la bibliothèque mutex. Lorsque le CPU, le contrôleur de mémoire, le noyau, le compilateur et la "bibliothèque C" sont tous implémentés par différentes équipes, et certains d'entre eux sont en désaccord violent sur la façon dont ces choses sont censées fonctionner, enfin, parfois les choses nous, les programmeurs de systèmes, devons faire pour présenter une jolie façade au niveau des applications ce n'est pas agréable du tout.
zwol
11
Malheureusement, il ne suffit pas de protéger vos structures de données avec de simples mutex s'il n'y a pas de modèle de mémoire cohérent dans votre langue. Il existe diverses optimisations du compilateur qui ont du sens dans un contexte à un seul thread, mais lorsque plusieurs threads et cœurs de processeur entrent en jeu, le réordonnancement des accès à la mémoire et d'autres optimisations peuvent donner un comportement indéfini. Pour plus d'informations, voir «Les threads ne peuvent pas être implémentés en tant que bibliothèque» par Hans Boehm: citeseer.ist.psu.edu/viewdoc/…
exDM69
0

Les réponses ci-dessus abordent les aspects les plus fondamentaux du modèle de mémoire C ++. Dans la pratique, la plupart des utilisations du std::atomic<>«travail juste», au moins jusqu'à ce que le programmeur sur-optimise (par exemple, en essayant de détendre trop de choses).

Il y a un endroit où les erreurs sont encore courantes: les verrous de séquence . Il y a une discussion excellente et facile à lire sur les défis à https://www.hpl.hp.com/techreports/2012/HPL-2012-68.pdf . Les verrous de séquence sont attrayants car le lecteur évite d'écrire sur le mot de verrouillage. Le code suivant est basé sur la figure 1 du rapport technique ci-dessus et met en évidence les défis lors de l'implémentation de verrous de séquence en C ++:

atomic<uint64_t> seq; // seqlock representation
int data1, data2;     // this data will be protected by seq

T reader() {
    int r1, r2;
    unsigned seq0, seq1;
    while (true) {
        seq0 = seq;
        r1 = data1; // INCORRECT! Data Race!
        r2 = data2; // INCORRECT!
        seq1 = seq;

        // if the lock didn't change while I was reading, and
        // the lock wasn't held while I was reading, then my
        // reads should be valid
        if (seq0 == seq1 && !(seq0 & 1))
            break;
    }
    use(r1, r2);
}

void writer(int new_data1, int new_data2) {
    unsigned seq0 = seq;
    while (true) {
        if ((!(seq0 & 1)) && seq.compare_exchange_weak(seq0, seq0 + 1))
            break; // atomically moving the lock from even to odd is an acquire
    }
    data1 = new_data1;
    data2 = new_data2;
    seq = seq0 + 2; // release the lock by increasing its value to even
}

Aussi peu intuitif que cela semble au début, data1et data2doit l'être atomic<>. S'ils ne sont pas atomiques, ils peuvent être lus (en reader()) en même temps qu'ils sont écrits (en writer()). Selon le modèle de mémoire C ++, il s'agit d'une course même si elle reader()n'utilise jamais réellement les données . De plus, s'ils ne sont pas atomiques, le compilateur peut mettre en cache la première lecture de chaque valeur dans un registre. Évidemment, vous ne voudriez pas cela ... vous voulez relire à chaque itération de la whileboucle reader().

Il ne suffit pas non plus d'en faire atomic<>et d'y accéder memory_order_relaxed. La raison en est que les lectures de seq (in reader()) n'ont acquis que la sémantique. En termes simples, si X et Y sont des accès à la mémoire, X précède Y, X n'est pas une acquisition ou une libération, et Y est une acquisition, alors le compilateur peut réorganiser Y avant X. Si Y était la deuxième lecture de seq, et X était une lecture de données, une telle réorganisation romprait l'implémentation du verrouillage.

Le document donne quelques solutions. Celui qui a les meilleures performances aujourd'hui est probablement celui qui utilise un atomic_thread_fenceavec memory_order_relaxed avant la deuxième lecture du seqlock. Dans le papier, c'est la figure 6. Je ne reproduis pas le code ici, parce que quiconque a lu jusqu'ici devrait vraiment lire le papier. Il est plus précis et complet que ce billet.

Le dernier problème est qu'il pourrait être contre nature de rendre les datavariables atomiques. Si vous ne pouvez pas dans votre code, alors vous devez être très prudent, car la conversion de non-atomique en atomique n'est légale que pour les types primitifs. C ++ 20 est censé être ajouté atomic_ref<>, ce qui facilitera la résolution de ce problème.

Pour résumer: même si vous pensez comprendre le modèle de mémoire C ++, vous devez être très prudent avant de lancer vos propres verrous de séquence.

Mike Spear
la source
-2

C et C ++ étaient définis par une trace d'exécution d'un programme bien formé.

Maintenant, ils sont à moitié définis par une trace d'exécution d'un programme, et à moitié a posteriori par de nombreux ordres sur des objets de synchronisation.

Cela signifie que ces définitions de langage n'ont aucun sens car aucune méthode logique pour mélanger ces deux approches. En particulier, la destruction d'un mutex ou d'une variable atomique n'est pas bien définie.

curiousguy
la source
Je partage votre ardent désir d'améliorer la conception du langage, mais je pense que votre réponse serait plus utile si elle était centrée sur un cas simple, pour lequel vous montriez clairement et explicitement comment ce comportement violait des principes spécifiques de conception du langage. Après cela, je vous recommande fortement, si vous me le permettez, de donner dans cette réponse une très bonne argumentation pour la pertinence de chacun de ces points, car ils seront mis en contraste avec la pertinence des avantages de productivité immenses perçus par la conception C ++
Matias Haeussler
1
@MatiasHaeussler Je pense que vous avez mal lu ma réponse; Je ne m'oppose pas à la définition d'une fonctionnalité C ++ particulière ici (j'ai aussi beaucoup de critiques pointues mais pas ici). Je soutiens ici qu'il n'y a pas de construction bien définie en C ++ (ni C). La sémantique MT entière est un gâchis complet, car vous n'avez plus de sémantique séquentielle. (Je crois que Java MT est cassé mais moins.) "L'exemple simple" serait presque n'importe quel programme MT. Si vous n'êtes pas d'accord, vous êtes invités à répondre à ma question sur la façon de prouver l'exactitude des programmes MT C ++ .
curiousguy
Intéressant, je pense que je comprends mieux ce que vous voulez dire après avoir lu votre question. Si j'ai raison, vous vous référez à l'impossibilité de développer des preuves pour l'exactitude des programmes C ++ MT . Dans un tel cas, je dirais que pour moi, c'est quelque chose d'énorme importance pour l'avenir de la programmation informatique, en particulier pour l'arrivée de l'intelligence artificielle. Mais je voudrais également souligner que pour la grande majorité des personnes qui posent des questions sur le débordement de pile, ce n'est même pas quelque chose dont elles sont conscientes, et même après avoir compris ce que vous voulez dire et devenir intéressé
Matias Haeussler
1
"Les questions sur la démostrabilité des programmes informatiques devraient-elles être publiées dans stackoverflow ou dans stackexchange (si dans aucun des deux, où)?" Celui-ci semble être celui du méta stackoverflow, n'est-ce pas?
Matias Haeussler
1
@MatiasHaeussler 1) C et C ++ partagent essentiellement le "modèle de mémoire" des variables atomiques, des mutex et du multithreading. 2) La pertinence à ce sujet concerne les avantages d'avoir le «modèle de mémoire». Je pense que l'avantage est nul car le modèle n'est pas sain.
curiousguy