In C ++ 11 wurde ein standardisiertes Speichermodell eingeführt. Was bedeutet das? Und wie wird sich das auf die C ++ - Programmierung auswirken?

1894

In C ++ 11 wurde ein standardisiertes Speichermodell eingeführt. Was genau bedeutet das? Und wie wird sich das auf die C ++ - Programmierung auswirken?

Dieser Artikel (von Gavin Clarke, der Herb Sutter zitiert ) sagt Folgendes:

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 verschiedene Threads mit dem Speicher 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 brechen, die die Leute im Code treffen werden", sagte Sutter .

Nun, ich kann mir diesen und ähnliche online verfügbare Absätze merken (da ich seit meiner Geburt mein eigenes Gedächtnismodell habe: P) und sogar als Antwort auf Fragen anderer posten, aber um ehrlich zu sein, verstehe ich das nicht genau diese.

C ++ - Programmierer haben bereits zuvor Multithread-Anwendungen entwickelt. Wie spielt es also eine Rolle, ob es sich um POSIX-Threads, Windows-Threads 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 irgendwie mit der Unterstützung von C ++ 11-Multithreading zusammenhängt, da ich diese beiden oft zusammen sehe. Wenn ja, wie genau? Warum sollten sie verwandt sein?

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

Nawaz
quelle
3
@curiousguy: Aufwändig ...
Nawaz
4
@curiousguy: Dann schreibe einen Blog ... und schlage auch eine Lösung vor. Es gibt keine andere Möglichkeit, Ihren Standpunkt gültig und begründet zu machen.
Nawaz
2
Ich habe diese Seite als einen Ort verwechselt, an dem ich Fragen stellen und Ideen austauschen kann. Mein Fehler; Es ist ein Ort der Konformität, an dem Sie Herb Sutter nicht widersprechen können, selbst wenn er sich offen über die Wurfspezifikation widerspricht.
Neugieriger
5
@curiousguy: C ++ ist das, was der Standard sagt, nicht das, was ein zufälliger Typ im Internet sagt. Also ja, es muss Konformität mit dem Standard geben. C ++ ist nicht eine offene Philosophie , wo man über alles reden kann , die es nicht konform zu den Standards.
Nawaz
3
"Ich habe bewiesen, dass kein C ++ - Programm ein genau definiertes Verhalten haben kann." . Hohe Ansprüche, ohne Beweise!
Nawaz

Antworten:

2205

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 besteht darin, diesen Code auf einer konkreten Maschine zu aktualisieren. Wenn Sie streng nach Spezifikation codieren, können Sie sicher sein, dass Ihr Code auf jedem System mit einem kompatiblen C ++ - Compiler kompiliert und ohne Änderungen ausgeführt wird, egal ob heute oder in 50 Jahren.

Die abstrakte Maschine in der C ++ 98 / C ++ 03-Spezifikation ist grundsätzlich Single-Threaded. Es ist daher nicht möglich, Multithread-C ++ - Code zu schreiben, der in Bezug auf die Spezifikation "vollständig portabel" ist. Die Spezifikation sagt nicht einmal etwas über die Atomizität aus von Speicherladevorgängen und -speichern oder die Reihenfolge aus, in der Ladevorgänge und Speicherungen auftreten können, ganz zu schweigen von Dingen wie Mutexen.

Natürlich können Sie in der Praxis Multithread-Code für bestimmte konkrete Systeme schreiben - wie z. B. Pthreads oder Windows. Aber es gibt keinen Standardmethode zum Schreiben von Multithread-Code für C ++ 98 / C ++ 03.

Die abstrakte Maschine in C ++ 11 ist vom Design her multithreaded. Es hat auch eine gut definierte Speichermodell ; Das heißt, es wird angegeben, was der Compiler beim Zugriff auf den Speicher tun darf und was nicht.

Betrachten Sie das folgende Beispiel, in dem zwei Threads gleichzeitig auf zwei globale 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 undefiniertes Verhalten. Die Frage selbst ist bedeutungslos da der Standard nichts betrachtet, was als "Thread" bezeichnet wird.

Unter C ++ 11 ist das Ergebnis Undefiniertes Verhalten, da Lasten und Speichern im Allgemeinen nicht atomar sein müssen. Was vielleicht nicht viel von einer Verbesserung zu sein scheint ... Und an sich ist es nicht.

Mit C ++ 11 können Sie jedoch 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 das Verhalten hier definiert . Thread 2 könnte jetzt gedruckt werden 0 0(wenn er vor Thread 1 ausgeführt wird),37 17 (wenn er nach Thread 1 ausgeführt wird) oder 0 17(wenn er ausgeführt wird, nachdem Thread 1 x zugewiesen wurde, aber bevor er y zugewiesen wurde).

Was nicht gedruckt werden kann, ist 37 0, dass der Standardmodus für atomare Ladevorgänge / Speicher in C ++ 11 darin besteht, die sequentielle Konsistenz zu erzwingen . Dies bedeutet nur, 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 es das System wünscht. Das Standardverhalten der Atomik bietet also sowohl Atomizität als auch Ordnung für Lasten und Speicher.

Auf einer modernen CPU kann die Sicherstellung der sequentiellen Konsistenz teuer sein. Insbesondere wird der Compiler wahrscheinlich zwischen jedem Zugriff hier vollständige Speicherbarrieren emittieren. Aber wenn Ihr Algorithmus nicht ordnungsgemäße Ladevorgänge und Speicher tolerieren kann; dh wenn es Atomizität erfordert, aber keine Ordnung; Das heißt, wenn es 37 0als Ausgabe dieses Programms toleriert werden kann, können Sie Folgendes 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 wahrscheinlicher ist dies schneller als im vorherigen Beispiel.

Wenn Sie nur bestimmte Ladungen und Speicher in Ordnung halten müssen, 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 37 0 ist also 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 wäre dies nicht der Fall.)

Natürlich, wenn die einzigen Ausgänge, die Sie sehen möchten, sind 0 0 oder sind 37 17, können Sie den Originalcode einfach mit einem Mutex umschließen. Aber wenn Sie so weit gelesen haben, wissen Sie bestimmt schon, wie das funktioniert, und diese Antwort ist bereits länger als beabsichtigt :-).

Unterm Strich also. Mutexe sind großartig und C ++ 11 standardisiert sie. Manchmal möchten Sie jedoch aus Leistungsgründen untergeordnete Grundelemente (z. B. das klassische doppelt überprüfte Sperrmuster ). Der neue Standard bietet Gadgets auf hoher Ebene wie Mutexe und Bedingungsvariablen sowie Gadgets auf niedriger Ebene wie Atomtypen und die verschiedenen Varianten der Speicherbarriere. Jetzt können Sie anspruchsvolle, leistungsstarke gleichzeitige Routinen vollständig in der vom Standard festgelegten Sprache schreiben und sicher sein, dass Ihr Code auf den Systemen von heute und morgen 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 in diesem Blogbeitrag .

Nemo
quelle
37
Schöne Antwort, aber dies bittet wirklich um einige tatsächliche Beispiele der neuen Primitiven. Ich denke auch, dass die Speicherreihenfolge ohne Grundelemente dieselbe ist wie vor C ++ 0x: Es gibt keine Garantien.
John Ripley
5
@ John: Ich weiß, aber ich lerne immer noch die Primitiven selbst :-). Ich denke auch, dass sie garantieren, dass Byte-Zugriffe atomar sind (obwohl nicht bestellt), weshalb ich für mein Beispiel "char" gewählt habe ... Aber da bin ich mir nicht einmal 100% sicher ... Wenn Sie etwas Gutes vorschlagen wollen " Tutorial "Referenzen Ich werde sie meiner Antwort hinzufügen
Nemo
48
@Nawaz: Ja! Speicherzugriffe können vom Compiler oder der CPU neu angeordnet werden. Denken Sie an (zB) Caches und spekulative Belastungen. Die Reihenfolge, in der der Systemspeicher getroffen wird, kann nicht mit der von Ihnen codierten Reihenfolge übereinstimmen. Der Compiler und die CPU stellen sicher, dass solche Neuordnungen den Single-Threaded- Code nicht beschädigen . Bei Multithread-Code charakterisiert das "Speichermodell" die möglichen Neuordnungen und was passiert, wenn zwei Threads gleichzeitig denselben Speicherort lesen / schreiben, und wie Sie die Kontrolle über beide ausüben. Für Single-Threaded-Code ist das Speichermodell irrelevant.
Nemo
26
@Nawaz, @Nemo - Ein kleines Detail: Das neue Speichermodell ist in Single-Threaded-Code insofern relevant, als es die Undefiniertheit bestimmter Ausdrücke angibt, wie z i = i++. Das alte Konzept der Sequenzpunkte wurde verworfen. Der neue Standard spezifiziert dasselbe unter Verwendung einer Sequenz-vor- Beziehung, die nur ein Sonderfall des allgemeineren Inter-Thread -Vor-Vor- Konzepts ist.
JohannesD
17
@ AJG85: In Abschnitt 3.6.2 des Entwurfs der C ++ 0x-Spezifikation heißt es: "Variablen mit statischer Speicherdauer (3.7.1) oder Thread-Speicherdauer (3.7.2) müssen vor jeder anderen Initialisierung mit Null initialisiert (8.5) werden Ort." Da x, y in diesem Beispiel global sind, haben sie eine statische Speicherdauer und werden daher meines Erachtens auf Null initialisiert.
Nemo
345

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 Nachdenken über Gedächtniskonsistenzmodelle erleichtert.

Lassen Sie uns die Historien aller Speicherorte in einem Raum-Zeit-Diagramm anzeigen, in dem die horizontale Achse den Adressraum darstellt (dh jeder Speicherort 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 diese Stelle schreibt. Mit einem Speicherbild ist die Aggregation / Kombination von Werten aller Speicherorte gemeint, die zu einem bestimmten Zeitpunkt von einem bestimmten Thread beobachtet werden können .

Zitat aus "Ein Leitfaden zur 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 zeitmultiplexiert würden.

Diese globale Speicherreihenfolge kann von Programmlauf zu Programmlauf variieren und ist möglicherweise nicht im Voraus bekannt. Das charakteristische Merkmal von SC ist der Satz horizontaler Schichten im Adressraum-Zeit-Diagramm, die Gleichzeitigkeitsebenen darstellen (dh Speicherbilder). Auf einer bestimmten Ebene sind alle Ereignisse (oder Speicherwerte) gleichzeitig. Es gibt eine Vorstellung von absoluten Zeitkombinationen , in dem sich alle Threads darauf einigen, welche Speicherwerte gleichzeitig vorliegen. 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 Speicherbild einig (dh über den Gesamtinhalt des Speichers). Dies bedeutet nicht nur, dass alle Threads für alle Speicherorte dieselbe Wertesequenz anzeigen, sondern auch, dass alle Prozessoren dieselbe beobachtenKombinationen von Werten aller Variablen. Dies entspricht der Aussage, dass alle Speicheroperationen (an allen Speicherorten) von allen Threads in derselben Gesamtreihenfolge beobachtet werden.

In entspannten Speichermodellen schneidet jeder Thread die Adressraumzeit auf seine eigene Weise auf. Die einzige Einschränkung besteht darin, dass sich die Slices jedes Threads nicht kreuzen dürfen, da sich alle Threads (natürlich) auf den Verlauf jedes einzelnen Speicherorts einigen müssen , Scheiben unterschiedlicher Fäden können und werden sich kreuzen). Es gibt keine universelle Möglichkeit, es aufzuteilen (keine privilegierte Folierung von Adressraum-Zeit). Slices müssen nicht planar (oder linear) sein. Sie können gekrümmt sein, und dies kann dazu führen, dass ein Thread die von einem anderen Thread geschriebenen Werte in der Reihenfolge liest, in der sie geschrieben wurden. Historien verschiedener Speicherorte können bei Betrachtung durch einen bestimmten Thread beliebig relativ zueinander verschoben (oder gedehnt) werden. Jeder Thread hat eine andere Vorstellung davon, welche Ereignisse (oder gleichwertig Speicherwerte) gleichzeitig auftreten. Die Ereignisse (oder Speicherwerte), die gleichzeitig mit einem Thread ausgeführt werden, sind nicht gleichzeitig mit einem anderen Thread. Somit beobachten in einem entspannten Speichermodell alle Threads immer noch den gleichen Verlauf (dh die Folge von Werten) für jeden Speicherort. Sie können jedoch unterschiedliche Speicherbilder beobachten (dh Kombinationen von Werten aller Speicherorte). Selbst wenn zwei verschiedene Speicherorte nacheinander von demselben Thread geschrieben werden, können die beiden neu geschriebenen Werte von anderen Threads in unterschiedlicher Reihenfolge beobachtet werden.

[Bild aus Wikipedia] Bild aus Wikipedia

Leser, die mit Einsteins spezieller Relativitätstheorie vertraut sind, werden bemerken, worauf ich anspreche. Minkowskis Worte in den Bereich der Speichermodelle übersetzen: Adressraum und Zeit sind Schatten der Adressraumzeit. In diesem Fall projiziert jeder Beobachter (dh Thread) Schatten von Ereignissen (dh Speicher / Speicher) auf seine eigene Weltlinie (dh seine Zeitachse) und seine eigene Ebene der Gleichzeitigkeit (seine Adressraumachse). . Threads im C ++ 11-Speichermodell entsprechen Beobachtern , die sich in spezieller Relativitätstheorie relativ zueinander bewegen. Die sequentielle Konsistenz entspricht der galiläischen Raumzeit (dh alle Beobachter sind sich über eine absolute Reihenfolge der Ereignisse und ein globales Gefühl der Gleichzeitigkeit einig).

Die Ähnlichkeit zwischen Speichermodellen und spezieller Relativitätstheorie beruht auf der Tatsache, dass beide eine teilweise geordnete Menge von Ereignissen definieren, die oft als kausale Menge bezeichnet wird. Einige Ereignisse (z. B. Speicher) können andere Ereignisse beeinflussen (aber nicht beeinflussen). Ein C ++ 11-Thread (oder Beobachter in der Physik) ist nicht mehr als eine Kette (dh eine vollständig geordnete Menge) von Ereignissen (z. B. Speicher wird geladen und an möglicherweise unterschiedliche Adressen gespeichert).

In der Relativitätstheorie wird eine gewisse Ordnung in das scheinbar chaotische Bild von teilweise geordneten Ereignissen wiederhergestellt, da die einzige zeitliche Reihenfolge, über die sich alle Beobachter einig sind, die Reihenfolge unter "zeitlichen" Ereignissen ist (dh jene Ereignisse, die im Prinzip durch ein Teilchen verbunden werden können, das langsamer wird als die Lichtgeschwindigkeit im Vakuum). Nur die zeitlichen Ereignisse werden immer geordnet. Zeit in Physik, Craig Callender .

Im C ++ 11-Speichermodell wird ein ähnlicher Mechanismus (das Erfassungsfreigabekonsistenzmodell) verwendet, um diese lokalen Kausalitätsbeziehungen herzustellen .

Um eine Definition der Speicherkonsistenz und eine Motivation für das Aufgeben von SC bereitzustellen, zitiere ich aus "Ein Leitfaden zur Speicherkonsistenz und Cache-Kohärenz".

Für eine gemeinsam genutzte Speichermaschine definiert das Speicherkonsistenzmodell das architektonisch sichtbare Verhalten ihres Speichersystems. Das Korrektheitskriterium für das Verhalten einer einzelnen Prozessorkernpartition zwischen „ einem korrekten Ergebnis “ und „ vielen falschen Alternativen “. Dies liegt daran, dass die Prozessorarchitektur 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 dies normalerweise viele korrekte Ausführungenwährend viele (mehr) falsche verboten werden. Die Möglichkeit mehrerer korrekter Ausführungen ist darauf zurückzuführen, dass die ISA die gleichzeitige Ausführung mehrerer Threads ermöglicht, häufig mit vielen möglichen rechtlichen Verschachtelungen von Anweisungen aus verschiedenen Threads.

Entspannte oder schwache Speicherkonsistenzmodelle werden durch die Tatsache motiviert, dass die meisten Speicherreihenfolgen in starken Modellen nicht erforderlich sind. Wenn ein Thread zehn Datenelemente und dann ein Synchronisationsflag aktualisiert, ist es Programmierern normalerweise egal, ob die Datenelemente in der Reihenfolge zueinander 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 Bestellflexibilität zu erfassen und nur die Aufträge beizubehalten, die Programmierer “ benötigen”, Um sowohl eine höhere Leistung als auch eine höhere Korrektheit zu erzielen. In bestimmten Architekturen werden beispielsweise FIFO-Schreibpuffer von jedem Kern verwendet, um die Ergebnisse festgeschriebener (zurückgezogener) Speicher zu speichern, bevor die Ergebnisse in die Caches geschrieben werden. Diese Optimierung verbessert die Leistung, verstößt jedoch gegen SC. Der Schreibpuffer verbirgt die Latenz der Wartung eines Speicherfehlers. Da Geschäfte üblich sind, ist es ein wichtiger Vorteil, dass die meisten von ihnen nicht ins Stocken geraten. Bei einem Single-Core-Prozessor kann ein Schreibpuffer architektonisch unsichtbar gemacht werden, indem sichergestellt wird, dass eine Last 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 erfolgt normalerweise, indem entweder der Wert des letzten Speichers an A bis zum Laden von A umgangen wird, wobei "letzter" durch die Programmreihenfolge bestimmt wird. oder durch Abwürgen einer Last von A, wenn sich ein Speicher für A im Schreibpuffer befindet. Wenn mehrere Kerne verwendet werden, verfügt jeder über einen eigenen Bypass-Schreibpuffer. Ohne Schreibpuffer ist die Hardware SC, mit Schreibpuffern jedoch nicht, wodurch Schreibpuffer in einem Multicore-Prozessor architektonisch sichtbar werden.

Eine Neuordnung von Filialen kann auftreten, wenn ein Kern über einen Nicht-FIFO-Schreibpuffer verfügt, mit dem Speicher in einer anderen Reihenfolge als der von ihnen eingegebenen Reihenfolge abreisen können. Dies kann auftreten, wenn der erste Speicher im Cache fehlt, während der zweite Treffer erzielt wird, oder wenn der zweite Speicher mit einem früheren Speicher (dh vor dem ersten Speicher) verschmelzen kann. Eine Neuanordnung von Last und Last kann auch auf dynamisch geplanten Kernen erfolgen, die Anweisungen außerhalb der Programmreihenfolge ausführen. Dies kann sich genauso verhalten wie das Neuordnen von Speichern auf einem anderen Kern (Können Sie ein Beispiel für die Verschachtelung zwischen zwei Threads finden?). Das Neuordnen einer früheren Ladung mit einem späteren Speicher (eine Neuordnung des Ladespeichers) kann viele falsche Verhaltensweisen verursachen, z. B. das Laden eines Werts nach dem Aufheben der Sperre, die ihn schützt (wenn der Speicher der Entsperrvorgang ist).

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

Im Gegensatz zur Konsistenz ist die Cache-Kohärenz für Software weder sichtbar noch erforderlich. Coherence versucht, die Caches eines Shared-Memory-Systems so funktional unsichtbar zu machen wie die Caches in einem Single-Core-System. Die korrekte Kohärenz stellt sicher, dass ein Programmierer nicht bestimmen kann, ob und wo ein System Caches hat, indem er die Ergebnisse von Ladevorgängen und Speichern analysiert. Dies liegt daran, dass durch korrekte Kohärenz sichergestellt wird, dass die Caches niemals ein neues oder anderes Funktionsverhalten ermöglichen (Programmierer können möglicherweise weiterhin mithilfe des Timings auf die wahrscheinliche Cache-Struktur schließenInformation). Der Hauptzweck von Cache-Kohärenzprotokollen besteht darin, die Single-Writer-Multiple-Reader (SWMR) für jeden Speicherort unveränderlich zu halten. Ein wichtiger Unterschied zwischen Kohärenz und Konsistenz besteht darin, dass die Kohärenz pro Speicherort angegeben wird, während die Konsistenz in Bezug auf alle Speicherorte angegeben wird.

In Fortsetzung unseres mentalen Bildes entspricht die SWMR-Invariante der physikalischen Anforderung, dass sich höchstens ein Teilchen an einem Ort befindet, es jedoch eine unbegrenzte Anzahl von Beobachtern an jedem Ort geben kann.

Ahmed Nassar
quelle
52
+1 für die Analogie mit spezieller Relativitätstheorie habe ich versucht, die gleiche Analogie selbst zu machen. Zu oft sehe ich Programmierer, die Thread-Code untersuchen und versuchen, das Verhalten als Operationen in verschiedenen Threads zu interpretieren, die in einer bestimmten Reihenfolge miteinander verschachtelt sind, und ich muss ihnen, nein, bei Multiprozessorsystemen den Begriff der Gleichzeitigkeit zwischen verschiedenen <s mitteilen > Referenzrahmen </ s> Threads sind jetzt bedeutungslos. Der Vergleich mit der speziellen Relativitätstheorie ist ein guter Weg, um die Komplexität des Problems zu respektieren.
Pierre Lebeaupin
71
Sollten Sie also zu dem Schluss kommen, dass das Universum mehrkernig ist?
Peter K
6
@PeterK: Genau :) Und hier ist eine sehr schöne Visualisierung dieses Zeitbildes durch den Physiker Brian Greene: youtube.com/watch?v=4BjGWLJNPcA&t=22m12s Dies ist "Die Illusion der Zeit [Vollständiger Dokumentarfilm]" in Minute 22 und 12 Sekunden.
Ahmed Nassar
2
Ist es nur ich oder wechselt er von einem 1D-Speichermodell (horizontale Achse) zu einem 2D-Speichermodell (Gleichzeitigkeitsebenen)? Ich finde das etwas verwirrend, aber vielleicht liegt das daran, dass ich kein Muttersprachler bin ... Immer noch eine sehr interessante Lektüre.
Auf Wiedersehen SE
Sie haben einen wesentlichen Teil vergessen: " durch Analyse der Ergebnisse von Ladungen und Lagern " ... ohne genaue Zeitangaben zu verwenden.
neugieriger Kerl
115

Dies ist jetzt eine mehrjährige Frage, aber da sie sehr beliebt ist, sollte eine fantastische Ressource erwähnt werden, um mehr über das C ++ 11-Speichermodell zu erfahren. Ich sehe keinen Sinn darin, seinen Vortrag zusammenzufassen, um eine weitere vollständige Antwort zu erhalten, aber da dies der Typ ist, der den Standard tatsächlich geschrieben hat, denke ich, dass es sich lohnt, den Vortrag anzuschauen.

Herb Sutter hat einen dreistündigen Vortrag über das C ++ 11-Speichermodell mit dem Titel "Atomwaffen <>", 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, Rennen und das Speichermodell
  2. Bestellung - Was: Erwerben und freigeben
  3. Bestellung - 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 Atomics

Der Vortrag geht nicht auf die API ein, sondern auf die Argumentation, den Hintergrund, unter der Haube und hinter den Kulissen (wussten Sie, dass dem Standard eine entspannte Semantik hinzugefügt wurde, nur weil POWER und ARM das synchronisierte Laden nicht effizient unterstützen?).

eran
quelle
10
Dieses Gespräch ist in der Tat fantastisch und die 3 Stunden, die Sie damit verbringen, es zu sehen, absolut wert.
ZunTzu
5
@ZunTzu: Bei den meisten Videoplayern können Sie die Geschwindigkeit auf das 1,25-, 1,5- oder sogar 2-fache des Originals einstellen.
Christian Severin
4
@eran habt ihr zufällig die Folien? Links auf den Diskussionsseiten von Kanal 9 funktionieren nicht.
Athos
2
@athos Ich habe sie nicht, sorry. Versuchen Sie, Kanal 9 zu kontaktieren. Ich glaube nicht, dass die Entfernung beabsichtigt war (ich vermute, dass sie den Link von Herb Sutter erhalten haben, der so veröffentlicht wurde, wie er ist, und er hat später die Dateien entfernt; aber das ist nur eine Spekulation ...).
Eran
75

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

Wenn Sie über POSIX-Threads oder Windows-Threads sprechen, ist dies eine Illusion, da es sich tatsächlich um x86-Threads handelt, da es sich um eine Hardwarefunktion handelt, die gleichzeitig ausgeführt wird. Das C ++ 0x-Speichermodell bietet Garantien, unabhängig davon, ob Sie mit x86, ARM, MIPS oder anderen Funktionen arbeiten.

Hündchen
quelle
28
Posix-Threads sind nicht auf x86 beschränkt. In der Tat waren die ersten Systeme, auf denen sie implementiert wurden, wahrscheinlich keine x86-Systeme. Posix-Threads sind systemunabhängig und auf allen Posix-Plattformen gültig. Es ist auch nicht wirklich wahr, dass es sich um eine Hardwareeigenschaft handelt, da Posix-Threads auch durch kooperatives Multitasking implementiert werden können. Aber natürlich treten die meisten Threading-Probleme nur bei Hardware-Threading-Implementierungen auf (und einige sogar nur bei Multiprozessor- / Multicore-Systemen).
Celtschk
57

Für Sprachen, die kein Speichermodell angeben , schreiben Sie Code für die Sprache und das Speichermodell, die von der Prozessorarchitektur angegeben werden. Der Prozessor kann sich dafür entscheiden, Speicherzugriffe für die Leistung neu zu ordnen. Also, wenn Ihr Programm Datenrennen hat (ein Datum Rennen ist , wenn es möglich ist , für mehrere Kerne / hyper-Threads desselben Speicher gleichzeitig zuzugreifen) dann Ihrem Programm ist nicht Cross - Plattform wegen seiner Abhängigkeit von dem Prozessor - Speicher - Modell. In den Handbüchern der Intel- oder AMD-Software finden Sie Informationen dazu, wie die Prozessoren Speicherzugriffe neu anordnen können.

Sehr wichtig ist, dass Sperren (und Parallelitätssemantik mit Sperren) normalerweise plattformübergreifend implementiert werden. Wenn Sie also Standardsperren in einem Multithread-Programm ohne Datenrennen verwenden, müssen Sie 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 ++ zu beheben. Http://msdn.microsoft.com/en-us/library/12a04hfd(v=vs .80) .aspx . Angesichts der Tatsache, dass Windows nur unter x86 / x64 ausgeführt wird, sagt dies jedoch nicht viel aus (Intel- und AMD-Speichermodelle machen es einfach und effizient, die Semantik zum Erfassen / Freigeben in einer Sprache zu implementieren).

Ritesh
quelle
2
Es stimmt, dass Windows, als die Antwort geschrieben wurde, nur unter x86 / x64 ausgeführt wird, Windows jedoch zu einem bestimmten Zeitpunkt unter IA64, MIPS, Alpha AXP64, PowerPC und ARM. Heute läuft es auf verschiedenen Versionen von ARM, die sich in Bezug auf den Speicher von x86 stark unterscheiden und bei weitem nicht so verzeihend sind.
Lorenzo Dematté
Dieser Link ist etwas defekt (sagt "Visual Studio 2005 Retired-Dokumentation" ). Möchtest du es aktualisieren?
Peter Mortensen
3
Es war nicht wahr, selbst als die Antwort geschrieben wurde.
Ben
" um gleichzeitig auf denselben Speicher zuzugreifen", um auf widersprüchliche Weise
zuzugreifen
27

Wenn Sie Mutexe verwenden, um alle Ihre Daten zu schützen, sollten Sie sich wirklich keine Sorgen machen müssen. Mutexe haben immer ausreichende Bestell- und Sichtbarkeitsgarantien geliefert.

Wenn Sie nun Atomics oder sperrfreie Algorithmen verwendet haben, müssen Sie über das Speichermodell nachdenken. Das Speichermodell beschreibt genau, wann Atomics Ordnungs- und Sichtbarkeitsgarantien bietet, und bietet tragbare Zäune für handcodierte Garantien.

Bisher wurden Atomics mithilfe von Compiler-Intrinsics oder einer höheren Bibliothek durchgeführt. Zäune wären mit CPU-spezifischen Anweisungen (Speicherbarrieren) ausgeführt worden.

Ninjalj
quelle
19
Das Problem zuvor war, dass es keinen Mutex gab (im Sinne des C ++ - Standards). Die einzigen Garantien, die Sie erhalten haben, waren vom Mutex-Hersteller. Dies war in Ordnung, solange Sie den Code nicht portiert haben (da geringfügige Änderungen an den Garantien schwer zu erkennen sind). Jetzt erhalten wir Garantien durch den Standard, der zwischen Plattformen portierbar sein sollte.
Martin York
4
@Martin: In jedem Fall ist eine Sache das Speichermodell und eine andere sind die Atomics- und Threading-Grundelemente, die über diesem Speichermodell ausgeführt werden.
Ninjalj
4
Mein Punkt war auch meistens, dass es vorher meistens kein Speichermodell auf Sprachebene gab, es war zufällig das Speichermodell der zugrunde liegenden CPU. Jetzt gibt es ein Speichermodell, das Teil der Kernsprache ist; OTOH, Mutexe und dergleichen können immer als Bibliothek erstellt werden.
Ninjalj
3
Es könnte auch ein echtes Problem für die Leute sein, die versuchen , die Mutex-Bibliothek zu schreiben . Wenn die CPU, der Speichercontroller, der Kernel, der Compiler und die "C-Bibliothek" von verschiedenen Teams implementiert werden und einige von ihnen heftige Meinungsverschiedenheiten darüber haben, wie dieses Zeug funktionieren soll, na ja, manchmal das Zeug Wir Systemprogrammierer müssen tun, um eine hübsche Fassade auf Anwendungsebene zu präsentieren, was überhaupt nicht angenehm ist.
zwol
11
Leider reicht es nicht aus, Ihre Datenstrukturen mit einfachen Mutexen zu schützen, wenn Ihre Sprache kein konsistentes Speichermodell enthält. Es gibt verschiedene Compiler-Optimierungen, die in einem Thread-Kontext sinnvoll sind. Wenn jedoch mehrere Threads und CPU-Kerne ins Spiel kommen, kann die Neuordnung von Speicherzugriffen und anderen Optimierungen zu undefiniertem Verhalten führen. Weitere Informationen finden Sie unter "Threads können nicht als Bibliothek implementiert werden" von Hans Boehm: citeseer.ist.psu.edu/viewdoc/…
exDM69
0

Die obigen Antworten beziehen sich auf die grundlegendsten Aspekte des C ++ - Speichermodells. In der Praxis std::atomic<>funktionieren die meisten Anwendungen "nur", zumindest bis der Programmierer überoptimiert (z. B. indem er versucht, zu viele Dinge zu entspannen).

Es gibt einen Ort, an dem Fehler immer noch häufig sind: Sequenzsperren . Unter https://www.hpl.hp.com/techreports/2012/HPL-2012-68.pdf finden Sie eine hervorragende und leicht zu lesende Diskussion der Herausforderungen . Sequenzsperren sind ansprechend, da der Leser das Schreiben in das Sperrwort vermeidet. Der folgende Code basiert auf Abbildung 1 des obigen technischen Berichts und zeigt die Herausforderungen bei der Implementierung von Sequenzsperren in C ++ auf:

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
}

So unintuitiv wie es zunächst scheint data1und sein data2muss atomic<>. Wenn sie nicht atomar sind, können sie reader()genau zur gleichen Zeit (in ) gelesen werden, in der sie geschrieben sind (in writer()). Nach dem C ++ - Speichermodell ist dies ein Rennen, auch wenn reader()die Daten nie tatsächlich verwendet werden . Wenn sie nicht atomar sind, kann der Compiler außerdem den ersten Lesevorgang jedes Werts in einem Register zwischenspeichern. Offensichtlich möchten Sie das nicht ... Sie möchten jede Iteration der whileSchleife erneut einlesenreader() .

Es reicht auch nicht aus, sie zu erstellen atomic<>und mit ihnen zuzugreifen memory_order_relaxed. Der Grund dafür ist , dass die von Seq liest (in reader()) nur haben acquire Semantik. In einfachen Worten, wenn X und Y Speicherzugriffe sind, X vor Y steht, X keine Erfassung oder Freigabe ist und Y eine Erfassung ist, kann der Compiler Y vor X neu anordnen. Wenn Y der zweite Lesevorgang von seq und X war Wenn Daten gelesen werden, würde eine solche Neuordnung die Sperrimplementierung unterbrechen.

Das Papier gibt einige Lösungen. Derjenige mit der besten Leistung heute ist wahrscheinlich derjenige, der ein atomic_thread_fencemit verwendetmemory_order_relaxed Derjenige vor dem zweiten Lesen des seqlock verwendet. In der Zeitung ist es Abbildung 6. Ich reproduziere den Code hier nicht, weil jeder, der bisher gelesen hat, die Zeitung wirklich lesen sollte. Es ist präziser und vollständiger als dieser Beitrag.

Das letzte Problem ist, dass es unnatürlich sein könnte, das zu machen data Variablen atomar . Wenn Sie Ihren Code nicht eingeben können, müssen Sie sehr vorsichtig sein, da das Umwandeln von nicht-atomar in atomar nur für primitive Typen zulässig ist. C ++ 20 soll hinzugefügt werden atomic_ref<>, wodurch dieses Problem leichter zu lösen ist.

Zusammenfassend: Selbst wenn Sie glauben, das C ++ - Speichermodell zu verstehen, sollten Sie sehr vorsichtig sein, bevor Sie Ihre eigenen Sequenzsperren setzen.

Mike Spear
quelle
-2

C und C ++ wurden früher durch eine Ausführungsspur eines wohlgeformten Programms definiert.

Jetzt werden sie zur Hälfte durch eine Ausführungsspur eines Programms und zur Hälfte durch viele Ordnungen auf Synchronisationsobjekten definiert.

Dies bedeutet, dass diese Sprachdefinitionen überhaupt keine sinnvolle Methode sind, um diese beiden Ansätze zu mischen. Insbesondere ist die Zerstörung eines Mutex oder einer atomaren Variablen nicht genau definiert.

Neugieriger
quelle
Ich teile Ihren heftigen Wunsch nach einer Verbesserung des Sprachdesigns, aber ich denke, Ihre Antwort wäre wertvoller, wenn sie sich auf einen einfachen Fall konzentrieren würde, für den Sie klar und explizit gezeigt haben, wie dieses Verhalten gegen bestimmte Prinzipien des Sprachdesigns verstößt. Danach würde ich Ihnen dringend empfehlen, wenn Sie mir erlauben, in dieser Antwort eine sehr gute Argumentation für die Relevanz jedes dieser Punkte zu geben, da sie der Relevanz der von C ++
Matias
1
@MatiasHaeussler Ich denke du hast meine Antwort falsch verstanden; Ich habe hier keine Einwände gegen die Definition eines bestimmten C ++ - Features (ich habe auch viele solche gezielten Kritikpunkte, aber nicht hier). Ich argumentiere hier, dass es in C ++ (oder C) kein genau definiertes Konstrukt gibt. Die gesamte MT-Semantik ist ein komplettes Durcheinander, da Sie keine sequentielle Semantik mehr haben. (Ich glaube, Java MT ist kaputt, aber weniger.) Das "einfache Beispiel" wäre fast jedes MT-Programm. Wenn Sie nicht einverstanden sind, können Sie gerne meine Frage beantworten, wie Sie die Richtigkeit von MT C ++ - Programmen nachweisen können .
Neugieriger
Interessant, ich denke, ich verstehe mehr, was Sie meinen, nachdem Sie Ihre Frage gelesen haben. Wenn ich recht habe, verweisen Sie auf die Unmöglichkeit, Beweise für die Richtigkeit von C ++ MT-Programmen zu entwickeln . In einem solchen Fall würde ich sagen, dass für mich etwas von großer Bedeutung für die Zukunft der Computerprogrammierung ist, insbesondere für die Ankunft künstlicher Intelligenz. Aber ich möchte auch darauf hinweisen, dass für die große Mehrheit der Leute, die Fragen im Stapelüberlauf stellen, dies nicht einmal bewusst ist, und selbst nachdem sie verstanden haben, was Sie meinen und interessiert sind
Matias Haeussler
1
"Sollten Fragen zur Demostrabilität von Computerprogrammen im Stackoverflow oder im Stackexchange gestellt werden (wenn in keinem, wo)?" Dieser scheint einer für den Meta-Stackoverflow zu sein, nicht wahr?
Matias Haeussler
1
@MatiasHaeussler 1) C und C ++ teilen im Wesentlichen das "Speichermodell" von atomaren Variablen, Mutexen und Multithreading. 2) Die Relevanz hierfür liegt in den Vorteilen des "Speichermodells". Ich denke, der Nutzen ist Null, da das Modell nicht in Ordnung ist.
Neugieriger