it-swarm.com.de

C ++ 11 führte ein standardisiertes Speichermodell ein. Was heißt das? Und wie wird sich das auf die C ++ - Programmierung auswirken?

C++ 11 führte ein standardisiertes Speichermodell ein, aber was genau bedeutet das? Und wie wird sich das auf die C++ - Programmierung auswirken?

Dieser Artikel (von Gavin Clarke , der Herb Sutter zitiert = ) sagt, dass

Das Speichermodell bedeutet, dass C++ - Code jetzt über eine standardisierte Bibliothek verfügt, die aufgerufen werden kann, unabhängig davon, wer den Compiler erstellt hat und auf welcher Plattform er ausgeführt wird. Es gibt eine Standardmethode, um zu steuern, wie unterschiedliche Threads mit dem Arbeitsspeicher des Prozessors kommunizieren.

"Wenn Sie über die Aufteilung von [Code] auf verschiedene Kerne sprechen, die im Standard enthalten sind, sprechen wir über das Speichermodell. Wir werden es optimieren, ohne die folgenden Annahmen zu verletzen, die die Leute im Code treffen werden", sagte Sutter .

Nun, ich kann diese und ähnliche online verfügbaren Absätze auswendig lernen (da ich seit meiner Geburt ein eigenes Gedächtnismodell habe: P) und kann sie sogar als Antwort auf Fragen posten, die von anderen gestellt wurden aber ehrlich gesagt verstehe ich das nicht genau.

C++ - Programmierer haben schon früher Multithread-Anwendungen entwickelt. Wie spielt es also eine Rolle, ob es sich um POSIX-, Windows- oder C++ 11-Threads handelt? Was sind die Vorteile? Ich möchte die Details auf niedriger Ebene verstehen.

Ich habe auch das Gefühl, dass das C++ 11-Speichermodell in irgendeiner Weise mit der Unterstützung von C++ 11-Multithreading zusammenhängt, da ich diese beiden häufig zusammen sehe. Wenn ja, wie genau? Warum sollten sie verwandt sein?

Da ich nicht weiß, wie die Interna von Multithreading funktionieren und was Speichermodell im Allgemeinen bedeutet, helfen Sie mir bitte, diese Konzepte zu verstehen. :-)

1770
Nawaz

Zuerst muss man lernen, wie ein Sprachanwalt zu denken.

Die C++ - Spezifikation bezieht sich nicht auf einen bestimmten Compiler, ein bestimmtes Betriebssystem oder eine bestimmte CPU. Es bezieht sich auf eine abstrakte Maschine , die eine Verallgemeinerung tatsächlicher Systeme darstellt. In der Welt der Sprachanwälte besteht die Aufgabe des Programmierers darin, Code für die abstrakte Maschine zu schreiben. Die Aufgabe des Compilers ist es, diesen Code auf einer konkreten Maschine zu aktualisieren. Wenn Sie streng nach der Spezifikation codieren, können Sie sicher sein, dass Ihr Code auf jedem System mit einem kompatiblen C++ - Compiler kompiliert und ohne Änderungen ausgeführt werden kann, egal ob heute oder in 50 Jahren.

Die abstrakte Maschine in der C++ 98/C++ 03-Spezifikation ist grundsätzlich ein Singlethread. Es ist also nicht möglich, Multithread-C++ - Code zu schreiben, der in Bezug auf die Spezifikation "vollständig portierbar" ist. Die Spezifikation sagt nicht einmal etwas über die Atomizität von Speicherladevorgängen und -speichern oder die Reihenfolge in denen Ladevorgänge und Speicher stattfinden könnten, egal, Dinge wie Mutexe.

Natürlich können Sie in der Praxis Multithread-Code für bestimmte konkrete Systeme schreiben - wie z. B. pthreads oder Windows. Es gibt jedoch keinen Standard Weg, um Multithread-Code für C++ 98/C++ 03 zu schreiben.

Die abstrakte Maschine in C++ 11 ist vom Design her multithreaded. Es hat auch ein gut definiertes Speichermodell ; Das heißt, es wird festgelegt, was der Compiler tun darf und was nicht, wenn er auf Speicher zugreift.

Betrachten Sie das folgende Beispiel, in dem zwei Threads gleichzeitig auf ein Paar globaler Variablen zugreifen:

           Global
           int x, y;

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

Was könnte Thread 2 ausgeben?

Unter C++ 98/C++ 03 ist dies nicht einmal ein undefiniertes Verhalten. Die Frage selbst ist bedeutungslos , da der Standard nichts in Betracht zieht, was als "Thread" bezeichnet wird.

Unter C++ 11 lautet das Ergebnis Undefiniertes Verhalten, da Laden und Speichern im Allgemeinen nicht atomar sein müssen. Das scheint vielleicht keine große Verbesserung zu sein ... und das ist es auch nicht.

Aber mit C++ 11 können Sie Folgendes schreiben:

           Global
           atomic<int> x, y;

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

Jetzt wird es viel interessanter. Zunächst wird hier das Verhalten definiert . Thread 2 könnte jetzt 0 0 drucken (wenn er vor Thread 1 ausgeführt wird), 37 17 (wenn er nach Thread 1 ausgeführt wird) oder 0 17 (wenn er nach Thread 1 ausgeführt wird, der x aber zugewiesen ist) bevor es y zuordnet).

Was nicht gedruckt werden kann, ist 37 0, da der Standardmodus für atomare Ladevorgänge/Speicher in C++ 11 darin besteht, sequentielle Konsistenz zu erzwingen . Dies bedeutet lediglich, dass alle Ladevorgänge und Speicher so sein müssen, als ob sie in der Reihenfolge geschehen wären, in der Sie sie in jedem Thread geschrieben haben, während Operationen zwischen Threads verschachtelt werden können, wie das System dies möchte. Das Standardverhalten von Atomics bietet also sowohl Atomicity als auch Ordering für Lasten und Shops.

Auf einer modernen CPU kann die Sicherstellung der sequentiellen Konsistenz teuer sein. Insbesondere der Compiler kann hier bei jedem Zugriff Speicherbarrieren ausbilden. Aber wenn Ihr Algorithmus nicht ordnungsgemäße Ladevorgänge und Speicher toleriert; d.h. wenn es Atomizität erfordert, aber nicht ordnet; wenn es 37 0 als Ausgabe von diesem Programm tolerieren kann, können Sie dies schreiben:

           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;

Je moderner die CPU, desto schneller als im vorherigen Beispiel.

Wenn Sie nur bestimmte Ladungen und Vorräte in Ordnung halten möchten, können Sie Folgendes schreiben:

           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;

Dies bringt uns zurück zu den bestellten Ladungen und Lagern - also ist 37 0 keine mögliche Ausgabe mehr - aber dies mit minimalem Overhead. (In diesem trivialen Beispiel entspricht das Ergebnis der vollständigen sequentiellen Konsistenz. In einem größeren Programm ist dies nicht der Fall.)

Wenn die einzigen Ausgaben, die Sie sehen möchten, 0 0 oder 37 17 sind, können Sie einfach einen Mutex um den Originalcode legen. Aber wenn Sie bis hierher gelesen haben, wissen Sie bestimmt schon, wie das funktioniert, und diese Antwort ist schon länger als beabsichtigt :-).

Unterm Strich also. Mutexe sind großartig und C++ 11 standardisiert sie. Manchmal möchten Sie jedoch aus Leistungsgründen Primitive niedrigerer Ebene (z. B. das klassische doppelt geprüftes Sperrmuster ). Der neue Standard bietet Gadgets auf hoher Ebene wie Mutexe und Bedingungsvariablen sowie Gadgets auf niedriger Ebene wie Atomtypen und die verschiedenen Arten von Speicherbarrieren. So können Sie jetzt ausgereifte, leistungsstarke, parallele Routinen vollständig in der vom Standard festgelegten Sprache schreiben und sicher sein, dass Ihr Code sowohl auf heutigen als auch auf zukünftigen Systemen unverändert kompiliert und ausgeführt wird.

Um ehrlich zu sein, sollten Sie sich wahrscheinlich an Mutexe und Bedingungsvariablen halten, es sei denn, Sie sind Experte und arbeiten an ernsthaftem Code auf niedriger Ebene. Das habe ich vor.

Weitere Informationen zu diesem Thema finden Sie unter dieser Blog-Beitrag .

2053
Nemo

Ich werde nur die Analogie geben, mit der ich Speicherkonsistenzmodelle (oder kurz Speichermodelle) verstehe. Es ist inspiriert von Leslie Lamports wegweisendem Artikel "Zeit, Uhren und die Reihenfolge von Ereignissen in einem verteilten System" . Die Analogie ist zutreffend und von grundlegender Bedeutung, kann aber für viele Menschen übertrieben sein. Ich hoffe jedoch, dass es ein mentales Bild (eine bildliche Darstellung) liefert, das das Denken über Speicherkonsistenzmodelle erleichtert.

Betrachten wir die Historien aller Speicherstellen in einem Raum-Zeit-Diagramm, in dem die horizontale Achse den Adressraum darstellt (dh jeder Speicherplatz wird durch einen Punkt auf dieser Achse dargestellt) und die vertikale Achse die Zeit darstellt (wir werden sehen, dass Im Allgemeinen gibt es keinen universellen Zeitbegriff. Der Verlauf der von jedem Speicherort gehaltenen Werte wird daher durch eine vertikale Spalte an dieser Speicheradresse dargestellt. Jede Wertänderung ist darauf zurückzuführen, dass einer der Threads einen neuen Wert an diesen Speicherort schreibt. Unter einem Speicherbild wird die Summe/Kombination von Werten aller beobachtbaren Speicherstellen verstanden zu einer bestimmten Zeit von ein bestimmter Thread .

Zitat aus "Eine Einführung in Speicherkonsistenz und Cache-Kohärenz"

Das intuitive (und restriktivste) Speichermodell ist die sequentielle Konsistenz (SC), bei der eine Multithread-Ausführung wie eine Verschachtelung der sequentiellen Ausführungen jedes einzelnen Threads aussehen sollte, als ob die Threads auf einem Single-Core-Prozessor zeitgemultiplext wären.

Diese globale Speicherreihenfolge kann von einem Programmlauf zum anderen variieren und ist möglicherweise nicht im Voraus bekannt. Das charakteristische Merkmal von SC ist die Menge der horizontalen Schichten im Adressraum-Zeit-Diagramm, die Gleichzeitigkeitsebenen darstellen. (dh Speicherbilder). In einer bestimmten Ebene sind alle Ereignisse (oder Speicherwerte) gleichzeitig. Es gibt eine Vorstellung von Absolute Time , in der alle Threads übereinstimmen, welche Speicherwerte gleichzeitig sind. In SC gibt es zu jedem Zeitpunkt nur ein Speicherabbild, das von allen Threads gemeinsam genutzt wird. Das heißt, zu jedem Zeitpunkt sind sich alle Prozessoren über das Speicherabbild einig (d. H. Den Gesamtinhalt des Speichers). Dies bedeutet nicht nur, dass alle Threads für alle Speicherorte dieselbe Wertereihenfolge anzeigen, sondern auch, dass alle Prozessoren dieselben Wertekombinationen aller Variablen beobachten. Dies ist das Gleiche wie die Aussage, dass alle Speicheroperationen (auf allen Speicherplätzen) von allen Threads in derselben Gesamtreihenfolge ausgeführt werden.

In entspannten Speichermodellen schneidet jeder Thread die Adressraumzeit auf seine eigene Weise auf. Die einzige Einschränkung besteht darin, dass sich die Segmente jedes Threads nicht kreuzen dürfen, da alle Threads über den Verlauf jedes einzelnen Speicherorts übereinstimmen müssen (natürlich) können und werden sich Scheiben verschiedener Fäden kreuzen). Es gibt keinen universellen Weg, um es aufzuteilen (keine privilegierte Foliation von Adressraumzeit). Schichten müssen nicht planar (oder linear) sein. Sie können gekrümmt sein und das ist es, was einen Thread dazu bringt, Werte zu lesen, die von einem anderen Thread in der Reihenfolge geschrieben wurden, in der sie geschrieben wurden. Historien verschiedener Speicherstellen können beliebig relativ zueinander gleiten (oder gedehnt werden) bei Betrachtung durch einen bestimmten Thread . Jeder Thread hat eine andere Vorstellung davon, welche Ereignisse (oder entsprechend Speicherwerte) gleichzeitig auftreten. Der Satz von Ereignissen (oder Speicherwerten), die gleichzeitig zu einem Thread gesendet werden, ist nicht gleichzeitig zu einem anderen Thread. Somit beobachten in einem entspannten Speichermodell alle Threads immer noch die gleiche Historie (d. H. Sequenz von Werten) für jeden Speicherort. Sie können jedoch unterschiedliche Speicherbilder beobachten (d. H. Kombinationen von Werten aller Speicherstellen). Selbst wenn zwei unterschiedliche Speicherstellen nacheinander von demselben Thread geschrieben werden, können die beiden neu geschriebenen Werte von anderen Threads in unterschiedlicher Reihenfolge beobachtet werden.

[Bild aus Wikipedia] Picture from Wikipedia

Leser, die mit Einsteins spezieller Relativitätstheorie vertraut sind, werden bemerken, worauf ich anspiele. Übersetzung von Minkowskis Worten in das Reich der Speichermodelle: Adressraum und -zeit sind Schatten der Adressraum-Zeit. In diesem Fall projiziert jeder Beobachter (dh Thread) Schatten von Ereignissen (dh Speichern/Laden von Speichern) auf seine eigene Weltlinie (dh seine Zeitachse) und seine eigene Gleichzeitigkeitsebene (seine Adressraumachse). . Threads im C++ 11-Speichermodell entsprechen Beobachtern , die sich in relativ zueinander bewegen Spezielle Relativität. Die sequentielle Konsistenz entspricht der galileischen Raumzeit (d. H. Alle Beobachter sind sich über eine absolute Reihenfolge von Ereignissen und ein globales Gefühl der Gleichzeitigkeit einig).

Die Ähnlichkeit zwischen Gedächtnismodellen und spezieller Relativitätstheorie beruht auf der Tatsache, dass beide eine teilweise geordnete Menge von Ereignissen definieren, die oft als Kausalsatz bezeichnet wird. Einige Ereignisse (d. H. Speicher) können andere Ereignisse beeinflussen (aber nicht von ihnen beeinflusst werden). Ein C++ 11-Thread (oder Beobachter in der Physik) ist nicht mehr als eine Kette (d. H. Eine vollständig geordnete Menge) von Ereignissen (z. B. Speicher lädt und speichert an möglicherweise unterschiedliche Adressen).

In der Relativitätstheorie wird eine gewisse Ordnung in das scheinbar chaotische Bild teilweise geordneter Ereignisse zurückgeführt, da die einzige zeitliche Ordnung, über die sich alle Beobachter einig sind, die Ordnung zwischen "zeitlichen" Ereignissen (dh solchen Ereignissen, die im Prinzip durch ein langsamer werdendes Teilchen verbunden werden können) ist als die Lichtgeschwindigkeit im Vakuum). Nur die zeitlich relevanten Ereignisse werden unveränderlich geordnet. Zeit in Physik, Craig Callender .

Im C++ 11-Speichermodell wird ein ähnlicher Mechanismus (das Konsistenzmodell von Erfassung und Freigabe) verwendet, um diese lokalen Kausalitätsbeziehungen .

Um eine Definition der Speicherkonsistenz und eine Motivation für den Abbruch von SC zu liefern, zitiere ich aus "Eine Einführung in Speicherkonsistenz und Cache-Kohärenz"

Für eine gemeinsam genutzte Speichermaschine definiert das Speicherkonsistenzmodell das architektonisch sichtbare Verhalten ihres Speichersystems. Das Korrektheitskriterium für ein einzelnes Prozessorkern-Partitionsverhalten zwischen „ einem korrekten Ergebnis “ und „ vielen falschen Alternativen “. Dies liegt daran, dass die Architektur des Prozessors vorschreibt, dass die Ausführung eines Threads einen bestimmten Eingabezustand in einen einzelnen, genau definierten Ausgabezustand umwandelt, selbst auf einem Kern außerhalb der Reihenfolge. Shared-Memory-Konsistenzmodelle betreffen jedoch das Laden und Speichern mehrerer Threads und ermöglichen normalerweise viele korrekte Ausführungen , während sie viele (mehr) falsche nicht zulassen. Die Möglichkeit mehrfacher korrekter Ausführungen ist darauf zurückzuführen, dass ISA mehrere Threads gleichzeitig ausführen kann, häufig mit vielen möglichen rechtlichen Verschachtelungen von Befehlen aus verschiedenen Threads.

Entspannt oder schwach Speicherkonsistenzmodelle werden durch die Tatsache motiviert, dass die meisten Speicherreihenfolgen in starken Modellen nicht erforderlich sind. Wenn ein Thread zehn Datenelemente und dann ein Synchronisierungsflag aktualisiert, ist es den Programmierern normalerweise egal, ob die Datenelemente in Bezug aufeinander aktualisiert werden, sondern nur, dass alle Datenelemente aktualisiert werden, bevor das Flag aktualisiert wird (normalerweise mithilfe von FENCE-Anweisungen implementiert) ). Entspannte Modelle versuchen, diese erhöhte Flexibilität bei der Bestellung zu erfassen und nur die Anweisungen beizubehalten, die Programmierer " benötigen ", um eine höhere Leistung und Korrektheit von SC zu erzielen. Beispielsweise werden in bestimmten Architekturen FIFO Schreibpuffer von jedem Kern verwendet, um die Ergebnisse von festgeschriebenen (zurückgezogenen) Speichern zu speichern, bevor die Ergebnisse in die Caches geschrieben werden. Diese Optimierung verbessert die Leistung, verstößt jedoch gegen SC. Der Schreibpuffer verbirgt die Wartezeit für die Bearbeitung eines Speicherfehlers. Da Geschäfte weit verbreitet sind, ist es ein wichtiger Vorteil, zu vermeiden, dass die meisten von ihnen stehen bleiben. Für einen Single-Core-Prozessor kann ein Schreibpuffer architektonisch unsichtbar gemacht werden, indem sichergestellt wird, dass ein Ladevorgang an Adresse A den Wert des letzten Speichers an A zurückgibt, selbst wenn sich ein oder mehrere Speicher an A im Schreibpuffer befinden. Dies geschieht typischerweise, indem entweder der Wert des letzten Speichers an A an das Laden von A übergeben wird, wobei "letzter" durch die Programmreihenfolge bestimmt wird, oder indem ein Laden von A angehalten wird, wenn sich ein Speicher an A im Schreibpuffer befindet . Wenn mehrere Kerne verwendet werden, verfügt jeder über einen eigenen Bypass-Schreibpuffer. Ohne Schreibpuffer ist die Hardware SC, aber mit Schreibpuffern nicht, wodurch Schreibpuffer in einem Multicore-Prozessor architektonisch sichtbar werden.

Eine Neuordnung zwischen Speichern und Speichern kann vorkommen, wenn ein Kern über einen Nicht-FIFO-Schreibpuffer verfügt, mit dem die Speicher in einer anderen Reihenfolge als der Reihenfolge, in der sie eingegeben wurden, verlassen können. Dies kann auftreten, wenn der erste Speicher im Cache fehlt, während der zweite Speicher aufschlägt, oder wenn der zweite Speicher mit einem früheren Speicher (d. H. Vor dem ersten Speicher) zusammengeführt werden kann. Das Umordnen von Ladevorgängen kann auch bei dynamisch geplanten Kernen erfolgen, die Anweisungen in einer anderen Reihenfolge als der Programmreihenfolge ausführen. Das kann sich genauso verhalten wie das Neuanordnen von Speichern auf einem anderen Kern (Können Sie ein Beispiel für das Verschachteln zwischen zwei Threads finden?). Die Neuordnung eines früheren Ladevorgangs mit einem späteren Speicher (eine Neuordnung des Ladevorgangs) kann zu vielen falschen Verhaltensweisen führen, z. B. zum Laden eines Werts nach dem Aufheben der Sperre, die den Ladevorgang schützt (wenn der Speicher die Entsperrungsoperation ist). Beachten Sie, dass Speicherladeumordnungen auch aufgrund der lokalen Umgehung des allgemein implementierten Schreibpuffers FIFO auftreten können, selbst wenn ein Kern alle Anweisungen in Programmreihenfolge ausführt.

Da Cache-Kohärenz und Speicherkonsistenz manchmal verwirrt sind, ist es aufschlussreich, auch dieses Zitat zu haben:

Im Gegensatz zur Konsistenz ist die Cache-Kohärenz weder für die Software sichtbar noch erforderlich. Durch Kohärenz sollen die Caches eines Shared-Memory-Systems so unsichtbar wie die Caches eines Single-Core-Systems gemacht werden. Die richtige Kohärenz stellt sicher, dass ein Programmierer nicht feststellen kann, ob und wo ein System Caches hat, indem er die Ergebnisse von Ladevorgängen und Speichern analysiert. Dies liegt daran, dass durch die korrekte Kohärenz sichergestellt wird, dass die Caches niemals ein neues oder anderes funktionales Verhalten ermöglichen (Programmierer können dies dennoch tun) in der Lage sein, eine wahrscheinliche Cache-Struktur unter Verwendung von Timing Informationen abzuleiten). Der Hauptzweck von Cache-Kohärenz-Protokollen besteht darin, die Single-Writer-Multiple-Readers (SWMR) für jeden Speicherort unveränderlich zu halten. Ein wichtiger Unterschied zwischen Kohärenz und Konsistenz besteht darin, dass die Kohärenz auf der Basis von Speicherorten angegeben wird. während die Konsistenz in Bezug auf alle Speicherstellen spezifiziert ist.

Um mit unserem mentalen Bild fortzufahren, entspricht die SWMR-Invariante der physikalischen Anforderung, dass sich höchstens ein Partikel an einem Ort befindet, es jedoch eine unbegrenzte Anzahl von Beobachtern an jedem Ort geben kann.

321
Ahmed Nassar

Dies ist jetzt eine mehrjährige Frage, aber da sie sehr beliebt ist, sollten Sie eine fantastische Ressource zum Erlernen des C++ 11-Speichermodells erwähnen. Ich sehe keinen Grund, seine Rede zusammenzufassen, um diese noch einmal vollständig zu beantworten, aber da dies der Typ ist, der den Standard tatsächlich geschrieben hat, denke ich, dass es sich lohnt, die Rede anzusehen.

Herb Sutter hält einen dreistündigen Vortrag über das C++ 11-Speichermodell mit dem Titel "atomic <> Weapons", das auf der Channel9-Site verfügbar ist - Teil 1 und Teil 2 . Der Vortrag ist ziemlich technisch und behandelt die folgenden Themen:

  1. Optimierungen, Rassen und das Speichermodell
  2. Bestellen - Was: Kaufen und Freigeben
  3. Bestellen - wie: Mutexe, Atomics und/oder Zäune
  4. Sonstige Einschränkungen für Compiler und Hardware
  5. Code Gen & Leistung: x86/x64, IA64, POWER, ARM
  6. Entspannte Atomik

Der Vortrag geht nicht auf die API ein, sondern auf die Überlegungen, Hintergründe, Hintergründe und Hintergründe (wussten Sie, dass entspannte Semantik nur deshalb zum Standard hinzugefügt wurde, weil POWER und ARM keine Unterstützung bieten Last effizient synchronisieren?).

104
eran

Dies bedeutet, dass der Standard jetzt Multithreading definiert und definiert, was im Kontext mehrerer Threads geschieht. Natürlich haben die Leute unterschiedliche Implementierungen verwendet, aber das ist wie die Frage, warum wir einen std::string haben sollten, wenn wir alle eine selbst gerollte string -Klasse verwenden könnten.

Wenn es sich um POSIX- oder Windows-Threads handelt, ist dies eine Illusion, da es sich tatsächlich um x86-Threads handelt, da es sich um eine Hardware-Funktion handelt, die gleichzeitig ausgeführt wird. Das C++ 0x-Speichermodell garantiert, ob Sie mit x86, ARM oder MIPS arbeiten oder mit allem anderen, was Sie sich einfallen lassen können.

73
Puppy

Für Sprachen, die kein Speichermodell angeben, schreiben Sie Code für die Sprache und das von der Prozessorarchitektur angegebene Speichermodell. Der Prozessor kann sich dafür entscheiden, Speicherzugriffe aus Leistungsgründen neu anzuordnen. Also, wenn Ihr Programm Datenrassen hat (eine Datenrasse ist, wenn es möglich ist, dass mehrere Kerne/Hyper-Threads gleichzeitig auf den gleichen Speicher zugreifen) dann Ihr Das Programm ist aufgrund seiner Abhängigkeit vom Prozessorspeichermodell nicht plattformübergreifend. In den Handbüchern zur Intel- oder AMD-Software erfahren Sie, wie die Prozessoren die Speicherzugriffe neu anordnen können.

Sehr wichtig ist, dass Sperren (und die Parallelitätssemantik mit Sperren) normalerweise plattformübergreifend implementiert werden. Wenn Sie also Standard-Sperren in einem Multithread-Programm ohne Datenrassen verwenden, müssen Sie nicht ' Sie müssen sich keine Gedanken über plattformübergreifende Speichermodelle machen .

Interessanterweise verfügen Microsoft-Compiler für C++ über eine Semantik für volatile, die eine C++ - Erweiterung darstellt, um das Fehlen eines Speichermodells in C++ http: //msdn.Microsoft.com/en-us/library) zu beheben /12a04hfd(v=vs.80).aspx . Angesichts der Tatsache, dass Windows nur auf x86/x64 ausgeführt wird, bedeutet dies nicht viel (Intel- und AMD-Speichermodelle vereinfachen und vereinfachen das Implementieren der Erfassungs-/Freigabesemantik in einer Sprache).

54
ritesh

Wenn Sie zum Schutz all Ihrer Daten Mutexe verwenden, sollten Sie sich wirklich keine Sorgen machen müssen. Mutexe haben immer ausreichende Bestell- und Sichtbarkeitsgarantien gegeben.

Wenn Sie jetzt Atomics oder Algorithmen ohne Sperren verwenden, müssen Sie über das Speichermodell nachdenken. Das Speichermodell beschreibt genau, wann Atomics Ordnungs- und Sichtbarkeitsgarantien bieten, und stellt tragbare Zäune für handcodierte Garantien bereit.

Früher wurden Atomics mithilfe von Compiler-Intrinsics oder einer Bibliothek höherer Ebenen erstellt. Zäune wären mit CPU-spezifischen Anweisungen (Speicherbarrieren) durchgeführt worden.

25
ninjalj

C und C++ wurden früher durch einen Ausführungs-Trace eines wohlgeformten Programms definiert.

Jetzt werden sie zur Hälfte durch einen Ausführungs-Trace eines Programms und zur Hälfte im Nachhinein durch viele Anordnungen von Synchronisationsobjekten definiert.

Das bedeutet, dass diese Sprachdefinitionen überhaupt keinen Sinn ergeben, da sie keine logische Methode zum Mischen dieser beiden Ansätze darstellen. Insbesondere ist die Zerstörung eines Mutex oder einer atomaren Variablen nicht genau definiert.

0
curiousguy