c++ - programmiersprachen - vorteile von c




C++ 11 führte ein standardisiertes Speichermodell ein. Was heißt das? Und wie wird es die C++ Programmierung beeinflussen? (4)

C ++ 11 führte ein standardisiertes Speichermodell ein, aber was genau bedeutet das? Und wie wird es die C ++ Programmierung beeinflussen?

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

Das Speichermodell bedeutet, dass C ++ - Code jetzt eine standardisierte Bibliothek zum Aufrufen hat, 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 das Teilen von [Code] über verschiedene Kerne im Standard sprechen, sprechen wir über das Speichermodell. Wir werden es optimieren, ohne die folgenden Annahmen zu brechen, die die Leute im Code machen werden", sagte Sutter .

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

Was ich im Grunde wissen möchte, ist, dass C ++ - Programmierer früher schon Multithread-Anwendungen entwickelt haben. Wie kommt es dann darauf an, ob es sich um POSIX-Threads oder 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 C ++ 11-Multithreading-Unterstützung verwandt ist, da ich diese beiden oft zusammen sehe. Wenn ja, wie genau? Warum sollten sie verwandt sein?

Da ich nicht weiß, wie Interna von Multi-Threading funktioniert und was Memory Model im Allgemeinen bedeutet, bitte helfen Sie mir, diese Konzepte zu verstehen. :-)

https://code.i-harness.com


Bei Sprachen, die kein Speichermodell angeben, schreiben Sie Code für die Sprache und das Speichermodell, das von der Prozessorarchitektur angegeben wird. Der Prozessor kann wählen, Speicherzugriffe für die Leistung neu zu ordnen. Wenn Ihr Programm also Datenrennen hat (ein Datenrennen ist, wenn mehrere Kerne / Hyper-Threads gleichzeitig auf denselben Speicher zugreifen können), ist Ihr Programm aufgrund seiner Abhängigkeit vom Prozessorspeichermodell nicht plattformübergreifend. In den Intel- oder AMD-Softwarehandbüchern finden Sie Informationen dazu, wie die Prozessoren Speicherzugriffe neu ordnen 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 nicht um plattformübergreifende Speichermodelle kümmern .

Interessanterweise haben Microsoft-Compiler für C ++ Semantik für flüchtige Elemente, die eine C ++ - Erweiterung ist, um mit dem Fehlen eines Speichermodells in C ++ umzugehen. http://msdn.microsoft.com/en-us/library/12a04hfd(v=vs.80).aspx . Da Windows jedoch nur auf x86 / x64 ausgeführt wird, sagt das nicht viel aus (Intel- und AMD-Speichermodelle machen es einfach und effizient, eine Semantik zum Akquirieren / Freigeben in einer Sprache zu implementieren).


Das bedeutet, dass der Standard jetzt Multithreading definiert und definiert, was im Kontext mehrerer Threads geschieht. Natürlich verwendeten Leute unterschiedliche Implementierungen, aber das ist wie die Frage, warum wir eine std::string wenn wir alle eine home-rolled string Klasse verwenden könnten.

Wenn Sie über POSIX-Threads oder Windows-Threads sprechen, ist das eine Illusion, da Sie eigentlich von x86-Threads sprechen, da es sich um eine Hardwarefunktion handelt, die gleichzeitig ausgeführt wird. Das C ++ 0x-Speichermodell garantiert, ob Sie auf x86 oder ARM oder MIPS stehen oder was Sie sonst noch brauchen.


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

Wenn Sie jetzt Atomics oder Lock-Free-Algorithmen verwenden, müssen Sie über das Speichermodell nachdenken. Das Speichermodell beschreibt genau, wann Atomics Bestell- und Sichtbarkeitsgarantien bietet und portable Zäune für handcodierte Garantien bietet.

Zuvor wurden Atomics mithilfe von Compiler-Intrinsics oder einer höheren Bibliotheksebene erstellt. Zäune wären mit CPU-spezifischen Anweisungen (Speicherbarrieren) gemacht worden.


Zuerst müssen Sie 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 der tatsächlichen Systeme ist. In der Welt der Sprachjuristen 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 kompiliert und ohne Änderung auf jedem System mit einem kompatiblen C ++ - Compiler ausgeführt wird, egal ob heute oder in 50 Jahren.

Die abstrakte Maschine in der Spezifikation C ++ 98 / C ++ 03 ist grundsätzlich single-threaded. Daher ist es nicht möglich, Multi-Thread-C ++ - Code zu schreiben, der in Bezug auf die Spezifikation "vollständig portierbar" ist. Die Spezifikation sagt noch nichts über die Atomizität von Speicherladungen und -speichern oder die Reihenfolge, in der Ladevorgänge und Speichervorgänge auftreten können, ganz zu schweigen von Dingen wie Mutexe.

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

Die abstrakte Maschine in C ++ 11 ist vom Entwurf her multi-threaded. Es hat auch ein wohldefiniertes Speichermodell ; Das heißt, was der Compiler tun kann und was nicht, wenn es darum geht, auf Speicher zuzugreifen.

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

           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 Undefined Behavior; Die Frage selbst ist bedeutungslos, weil der Standard nichts als "Thread" bezeichnet.

Unter C ++ 11 ist das Ergebnis Undefined Behavior, weil Lasten und Speicher im Allgemeinen nicht atomar sein müssen. Was nicht wie eine Verbesserung erscheinen mag ... Und das ist es nicht.

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

           Global
           atomic<int> x, y;

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

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

Was es nicht drucken kann, ist 37 0 , weil der Standardmodus für atomare Ladungen / Speicher in C ++ 11 sequentielle Konsistenz erzwingt. Das bedeutet nur, dass alle Ladevorgänge und Speichervorgänge "so als ob" sie in der Reihenfolge geschehen würden, in der Sie sie in jedem Thread geschrieben haben, während Operationen zwischen Threads verschachtelt werden können, wie es das System auch mag. Das Standardverhalten von Atomics stellt also sowohl die Atomarität als auch die Reihenfolge für Lasten und Speicher bereit.

Auf einer modernen CPU kann die Sicherstellung der sequentiellen Konsistenz teuer sein. Insbesondere wird der Compiler wahrscheinlich zwischen jedem Zugriff volle Speicherbarrieren aussenden. Aber wenn Ihr Algorithmus Out-Of-Order-Lasten und -Speicher tolerieren kann; dh wenn es Atomizität erfordert, aber nicht Ordnung; dh wenn es 37 0 als Ausgabe von diesem Programm tolerieren kann, dann 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 ist, desto wahrscheinlicher ist es, dass sie schneller ist als das vorherige Beispiel.

Schließlich, wenn Sie nur bestimmte Lasten und Speicher in der richtigen Reihenfolge halten müssen, können Sie 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 geordneten Ladungen und speichert - so ist 37 0 keine mögliche Ausgabe mehr - aber dies mit minimalem Overhead. (In diesem trivialen Beispiel ist das Ergebnis dasselbe wie die vollständige sequenzielle Konsistenz; in einem größeren Programm wäre dies nicht der Fall.)

Wenn die einzigen Ausgaben, die Sie sehen möchten, 0 0 oder 37 17 , können Sie natürlich einen Mutex um den ursprünglichen Code wickeln. Aber wenn du so weit gelesen hast, wette ich, du weißt bereits, wie das funktioniert, und diese Antwort ist schon länger, als ich es beabsichtigt hatte :-).

Also, unter dem Strich. Mutexe sind großartig und C ++ 11 standardisiert sie. Aber manchmal wollen Sie aus Gründen der Performance niedrigere Level-Primitive (zB das klassische Double-Checked-Locking-Pattern ). Der neue Standard bietet High-Level-Gadgets wie Mutexe und Zustandsvariablen und bietet auch Low-Level-Gadgets wie atomare Typen und die verschiedenen Geschmacksrichtungen der Speicherbarriere. Jetzt können Sie anspruchsvolle, hochperformante simultane Routinen vollständig in der vom Standard spezifizierten Sprache schreiben, und Sie können sicher sein, dass Ihr Code sowohl auf den heutigen als auch den heutigen Systemen kompiliert und unverändert bleibt.

Obwohl man ehrlich ist, sollte man sich, wenn man kein Experte ist und an einem ernsthaften Low-Level-Code arbeitet, wahrscheinlich an Mutexe und Condition-Variablen halten. Das beabsichtige ich zu tun.

Weitere Informationen zu diesem Thema finden Sie in diesem Blogbeitrag .





memory-model