In der Dokumentation von std::memory_order
cppreference.com finden Sie ein Beispiel für eine entspannte Bestellung:
Entspannte Bestellung
Mit Tags versehene atomare Operationen
memory_order_relaxed
sind keine Synchronisationsoperationen. Sie legen keine Reihenfolge bei gleichzeitigen Speicherzugriffen fest. Sie garantieren nur die Konsistenz der Atomizität und der Änderungsreihenfolge.Zum Beispiel, wenn x und y anfänglich Null sind,
// Thread 1: r1 = y.load(std::memory_order_relaxed); // A x.store(r1, std::memory_order_relaxed); // B // Thread 2: r2 = x.load(std::memory_order_relaxed); // C y.store(42, std::memory_order_relaxed); // D
darf r1 == r2 == 42 erzeugen, denn obwohl A vor B in Thread 1 und C vor D in Thread 2 sequenziert wird , hindert nichts D daran, vor A in der Änderungsreihenfolge von y und B vor zu erscheinen erscheint vor C in der Änderungsreihenfolge von x. Die Nebenwirkung von D auf y könnte für die Last A in Gewinde 1 sichtbar sein, während die Nebenwirkung von B auf x für die Last C in Gewinde 2 sichtbar sein könnte. Dies kann insbesondere auftreten, wenn D vor C in abgeschlossen ist Thread 2, entweder aufgrund einer Neuordnung des Compilers oder zur Laufzeit.
es heißt "C wird vor D in Thread 2 sequenziert".
Gemäß der Definition von "vorher sequenziert", die in der Reihenfolge der Bewertung zu finden ist, wird die Bewertung von A abgeschlossen, bevor die Bewertung von B beginnt, wenn A vor B sequenziert wird. Da C vor D in Thread 2 sequenziert wird, muss C abgeschlossen sein, bevor D beginnt, daher wird der Bedingungsteil des letzten Satzes des Schnappschusses niemals erfüllt.
quelle
Antworten:
Ich glaube, dass die Präferenz richtig ist. Ich denke, das läuft auf die "Als-ob" -Regel [intro.execution] / 1 hinaus . Compiler sind nur verpflichtet, das beobachtbare Verhalten des in Ihrem Code beschriebenen Programms zu reproduzieren. Eine Sequenz-vor- Beziehung wird nur zwischen Bewertungen aus der Perspektive des Threads hergestellt, in dem diese Bewertungen durchgeführt werden [intro.execution] / 15 . Das heißt, wenn zwei nacheinander sequenzierte Auswertungen irgendwo in einem Thread erscheinen, muss sich der tatsächlich in diesem Thread ausgeführte Code so verhalten, als ob die erste Auswertung tatsächlich die zweite Auswertung beeinflusst. Zum Beispiel
muss gedruckt werden 42. Der Compiler muss den Wert 42 jedoch nicht in einem Objekt speichern,
x
bevor er den Wert von diesem Objekt zurückliest, um ihn zu drucken. Es kann sich auch daran erinnern, dass der letzte Wert, in dem gespeichertx
wurde, 42 war, und dann einfach den Wert 42 direkt ausdrucken, bevor eine tatsächliche Speicherung des Werts 42 an durchgeführt wirdx
. Wennx
es sich um eine lokale Variable handelt, kann sie auch einfach nachverfolgen, welcher Wert dieser Variablen zu einem beliebigen Zeitpunkt zuletzt zugewiesen wurde, und niemals ein Objekt erstellen oder den Wert 42 tatsächlich speichern. Der Thread kann den Unterschied nicht erkennen. Das Verhalten wird immer so sein, als ob es eine Variable gäbe und als ob der Wert 42 tatsächlich zuvor in einem Objekt gespeichertx
worden wärevon diesem Objekt geladen werden. Dies bedeutet jedoch nicht, dass der generierte Maschinencode tatsächlich irgendwo irgendwo etwas speichern und laden muss. Alles, was erforderlich ist, ist, dass das beobachtbare Verhalten des generierten Maschinencodes nicht von dem zu unterscheiden ist, was das Verhalten wäre, wenn all diese Dinge tatsächlich passieren würden.Wenn wir schauen
dann ja, C wird vor D sequenziert. Aber wenn man diesen Thread isoliert betrachtet, beeinflusst nichts, was C tut, das Ergebnis von D. Und nichts, was D tut, würde das Ergebnis von C ändern. Der einzige Weg, wie einer den anderen beeinflussen könnte, wäre als indirekte Folge von etwas, das in einem anderen Thread passiert. Durch Angabe
std::memory_order_relaxed
haben Sie jedoch ausdrücklich angegebendass die Reihenfolge, in der das Laden und Speichern von einem anderen Thread beobachtet wird, irrelevant ist. Da kein anderer Thread das Laden und Speichern in einer bestimmten Reihenfolge beobachten kann, kann kein anderer Thread dafür sorgen, dass C und D sich auf konsistente Weise gegenseitig beeinflussen. Somit ist die Reihenfolge, in der das Laden und Speichern tatsächlich ausgeführt wird, irrelevant. Somit kann der Compiler sie neu anordnen. Und wie in der Erklärung unter diesem Beispiel erwähnt, kann r1 == r2 == 42 tatsächlich zustande kommen, wenn der Speicher von D vor dem Laden von C ausgeführt wird…quelle
Es ist manchmal möglich, dass eine Aktion relativ zu zwei anderen Sequenzen von Aktionen angeordnet wird, ohne dass eine relative Reihenfolge der Aktionen in diesen Sequenzen relativ zueinander impliziert wird.
Angenommen, man hat zum Beispiel die folgenden drei Ereignisse:
und das Lesen von p2 wird unabhängig nach dem Schreiben von p1 und vor dem Schreiben von p3 angeordnet, aber es gibt keine bestimmte Reihenfolge, an der sowohl p1 als auch p3 teilnehmen. Abhängig davon, was mit p2 gemacht wird, kann es für einen Compiler unpraktisch sein, p1 nach p3 zu verschieben und dennoch die erforderliche Semantik mit p2 zu erreichen. Angenommen, der Compiler wusste jedoch, dass der obige Code Teil einer größeren Sequenz war:
In diesem Fall könnte es bestimmen, dass es den Speicher nach dem obigen Code auf p1 umordnen und ihn mit dem folgenden Speicher konsolidieren könnte, was zu Code führt, der p3 schreibt, ohne zuerst p1 zu schreiben:
Obwohl es den Anschein haben mag, dass Datenabhängigkeiten dazu führen würden, dass sich bestimmte Teile der Sequenzierungsbeziehungen transitiv verhalten, kann ein Compiler Situationen identifizieren, in denen offensichtliche Datenabhängigkeiten nicht existieren, und würde daher nicht die erwarteten transitiven Effekte haben.
quelle
Wenn zwei Anweisungen vorhanden sind, generiert der Compiler den Code in sequentieller Reihenfolge, sodass der Code für die erste vor der zweiten platziert wird. CPU verfügt jedoch intern über Pipelines und kann Montagevorgänge parallel ausführen. Anweisung C ist eine Ladeanweisung. Während der Speicher abgerufen wird, verarbeitet die Pipeline die nächsten Anweisungen und da sie nicht von der Ladeanweisung abhängig sind, können sie ausgeführt werden, bevor C beendet ist (z. B. Daten für D befanden sich im Cache, C im Hauptspeicher).
Wenn der Benutzer die beiden Anweisungen wirklich benötigt, um nacheinander ausgeführt zu werden, können strengere Speicherreihenfolgeoperationen verwendet werden. Im Allgemeinen ist es den Benutzern egal, solange das Programm logisch korrekt ist.
quelle
Was auch immer Sie denken, ist gleichermaßen gültig. Der Standard sagt nicht, was sequentiell ausgeführt wird, was nicht und wie es verwechselt werden kann .
Es liegt an Ihnen und jedem einzelnen Programmierer, eine konsistente Semantik zu erstellen, die über dieses Durcheinander hinausgeht, eine Arbeit, die mehrere Doktorarbeiten verdient.
quelle