C ++ std :: memory_order_relaxed Verwirrung

Oct 12 2020

Ich habe diesen Artikel aus dem GCC-Wiki über C ++ - Speicherbarrieren gelesen (und es ist großartig).
Es war ziemlich einfach, bis ich zu diesem Punkt kam:

Der umgekehrte Ansatz ist std :: memory_order_relaxed. Dieses Modell ermöglicht eine wesentlich geringere Synchronisation, indem die Vor-Ort-Einschränkungen aufgehoben werden. Bei diesen Arten von atomaren Operationen können auch verschiedene Optimierungen durchgeführt werden, z. B. das Entfernen toter Speicher und das Commoning. Also im vorherigen Beispiel:

-Thread 1-
y.store (20, memory_order_relaxed)
x.store (10, memory_order_relaxed)

-Thread 2-
if (x.load (memory_order_relaxed) == 10)
  {
    assert (y.load(memory_order_relaxed) == 20) /* assert A */
    y.store (10, memory_order_relaxed)
  }

-Thread 3-
if (y.load (memory_order_relaxed) == 10)
  assert (x.load(memory_order_relaxed) == 10) /* assert B */

Da Threads nicht systemweit synchronisiert werden müssen, kann jede Behauptung in diesem Beispiel tatsächlich fehlschlagen.

Ok, das ist auch unkompliziert, lass uns weitermachen.

-Thread 1-
x.store (1, memory_order_relaxed)
x.store (2, memory_order_relaxed)

-Thread 2-
y = x.load (memory_order_relaxed)
z = x.load (memory_order_relaxed)
assert (y <= z)

Die Behauptung kann nicht fehlschlagen. Sobald der Speicher von 2 von Thread 2 gesehen wird, kann er den Wert 1 nicht mehr sehen. Dies verhindert, dass entspannte Lasten einer Variablen über entspannte Lasten einer anderen Referenz, die möglicherweise einen Alias ​​aufweist, zusammengeführt werden.

Dies hat mich verwirrt, warum y den Wert 2 nicht laden kann und z den Wert 1 lädt (und die Bestätigung fehlschlägt), da die Reihenfolge in Thread 1 nicht synchronisiert ist.

Antworten

4 NicolBolas Oct 12 2020 at 12:12

Die entspannte Reihenfolge bezieht sich auf die Reihenfolge der Operationen in Bezug auf andere Speicherzugriffe und nicht auf die Reihenfolge, in der das Atom entspannt ist. In Ihrem ersten Fall bedeutet die Tatsache, dass Sie 10 in sehen können x, nichts in Bezug auf den Wert von y. Und umgekehrt.

Ihr zweiter Fall ist jedoch anders, da er dasselbe atomare Objekt betrifft.

[intro.races] / 10 sagt uns, dass innerhalb eines Threads, wenn eine Operation vor einer anderen sequenziert wird, diese Operation vor der anderen "stattfindet". Und [intro.races] / 14-17 beschreiben das folgende Verhalten in Bezug auf Atomics :

Die vier vorhergehenden Kohärenzanforderungen verbieten effektiv die Neuordnung von atomaren Operationen durch den Compiler zu einem einzelnen Objekt, selbst wenn beide Operationen entspannte Lasten sind.

Und das haben Sie hier. Alle Änderungen werden am selben Objekt vorgenommen, daher müssen sie in einer bestimmten Reihenfolge erfolgen. Auch wenn diese Reihenfolge nicht genau bestimmt werden kann, muss die Reihenfolge die "passiert vor" -Beziehungen des Codes berücksichtigen.

Die beiden Operationen von Thread 1 sind nach einer Beziehung "Vorher passiert" geordnet. Und die Operationen von Thread 2 sind selbst nach einer "Vorher-Beziehung" geordnet.

Da sie alle auf dasselbe Atomobjekt einwirken und yden Wert 2 erhalten, muss "passiert nach" xauf 2 gesetzt worden sein. Die Zugriffsreihenfolge für xmuss also "x = 1, x = 2," gewesen sein. lies x ". Und da das letzte Lesen xnach dem ersten Lesen erfolgt x, kann der Wert, den es erhält, nicht 1 sein.