Was ist mit C ++ std :: atomic auf Programmiererebene garantiert?

9

Ich habe mehrere Artikel, Vorträge und Fragen zum Stackoverflow angehört und gelesen std::atomicund möchte sichergehen, dass ich sie gut verstanden habe. Weil ich immer noch ein bisschen verwirrt bin mit der Sichtbarkeit von Cache-Zeilen-Schreibvorgängen aufgrund möglicher Verzögerungen bei MESI-Cache-Kohärenzprotokollen (oder abgeleiteten Protokollen), Speicherpuffern, ungültigen Warteschlangen usw.

Ich habe gelesen, dass x86 ein stärkeres Speichermodell hat und dass x86 gestartete Vorgänge zurücksetzen kann, wenn sich eine Cache-Ungültigmachung verzögert. Jetzt interessiert mich aber nur noch, was ich als C ++ - Programmierer unabhängig von der Plattform annehmen soll.

[T1: Thread1 T2: Thread2 V1: gemeinsame atomare Variable]

Ich verstehe, dass std :: atomic dies garantiert,

(1) Für eine Variable treten keine Datenrennen auf (dank des exklusiven Zugriffs auf die Cache-Zeile).

(2) Je nachdem, welche Speicherreihenfolge wir verwenden, wird (mit Barrieren) garantiert, dass eine sequentielle Konsistenz auftritt (vor einer Barriere, nach einer Barriere oder beidem).

(3) Nach einem atomaren Schreiben (V1) auf T1 ist ein atomares RMW (V1) auf T2 kohärent (seine Cache-Zeile wurde mit dem geschriebenen Wert auf T1 aktualisiert).

Aber als Cache-Kohärenz-Primer erwähnen,

All diese Dinge haben zur Folge, dass beim Laden standardmäßig veraltete Daten abgerufen werden können (wenn sich eine entsprechende Ungültigkeitsanforderung in der Ungültigkeitswarteschlange befand).

Ist das Folgende richtig?

(4) std::atomicgarantiert NICHT, dass T2 bei einem atomaren Lesevorgang (V) nach einem atomaren Schreibvorgang (V) bei T1 keinen "veralteten" Wert liest.

Fragen, ob (4) richtig ist: Wenn der atomare Schreibvorgang auf T1 die Cache-Zeile ungeachtet der Verzögerung ungültig macht, warum wartet T2 darauf, dass die Ungültigmachung wirksam wird, wenn eine atomare RMW-Operation ausgeführt wird, jedoch nicht bei einem atomaren Lesevorgang?

Fragen, wenn (4) falsch ist: Wann kann ein Thread einen 'veralteten' Wert lesen und "es ist sichtbar" in der Ausführung?

Ich schätze Ihre Antworten sehr

Update 1

Es scheint also, dass ich mich damals in (3) geirrt habe. Stellen Sie sich die folgende Verschachtelung für eine anfängliche V1 = 0 vor:

T1: W(1)
T2:      R(0) M(++) W(1)

Obwohl das RMW von T2 in diesem Fall garantiert vollständig nach W (1) auftritt, kann es dennoch einen "veralteten" Wert lesen (ich habe mich geirrt). Demnach garantiert Atomic keine vollständige Cache-Kohärenz, sondern nur sequentielle Konsistenz.

Update 2

(5) Stellen Sie sich nun dieses Beispiel vor (x = y = 0 und atomar):

T1: x = 1;
T2: y = 1;
T3: if (x==1 && y==0) print("msg");

Laut dem, was wir gesprochen haben, würde uns das Anzeigen der "Nachricht" auf dem Bildschirm keine Informationen darüber geben, dass T2 nach T1 ausgeführt wurde. Eine der folgenden Hinrichtungen könnte also stattgefunden haben:

  • T1 <T3 <T2
  • T1 <T2 <T3 (wobei T3 x = 1 sieht, aber noch nicht y = 1)

ist das richtig?

(6) Wenn ein Thread immer "veraltete" Werte lesen kann, was würde passieren, wenn wir das typische "Veröffentlichen" -Szenario verwenden, aber anstatt zu signalisieren, dass einige Daten bereit sind, machen wir genau das Gegenteil (löschen Sie die Daten)?

T1: delete gameObjectPtr; is_enabled.store(false, std::memory_order_release);
T2: while (is_enabled.load(std::memory_order_acquire)) gameObjectPtr->doSomething();

Dabei würde T2 immer noch einen gelöschten ptr verwenden, bis festgestellt wird, dass is_enabled false ist.

(7) Auch die Tatsache, dass Threads "veraltete" Werte lesen können, bedeutet, dass ein Mutex nicht mit nur einem sperrenfreien Atom implementiert werden kann, oder? Es würde einen Synchronisationsmechanismus zwischen Threads erfordern. Würde es ein abschließbares Atom erfordern?

Albert Caldas
quelle

Antworten:

3
  1. Ja, es gibt keine Datenrennen
  2. Ja, mit geeigneten memory_orderWerten können Sie die sequentielle Konsistenz garantieren
  3. Ein atomares Lesen-Ändern-Schreiben erfolgt immer vollständig vor oder vollständig nach einem atomaren Schreiben in dieselbe Variable
  4. Ja, T2 kann nach einem atomaren Schreibvorgang auf T1 einen veralteten Wert aus einer Variablen lesen

Atomare Lese-, Änderungs- und Schreiboperationen werden so spezifiziert, dass ihre Atomizität gewährleistet ist. Wenn ein anderer Thread nach dem ersten Lesen und vor dem Schreiben einer RMW-Operation auf den Wert schreiben könnte, wäre diese Operation nicht atomar.

Threads können immer veraltete Werte lesen, es sei denn, dies geschieht, bevor die relative Reihenfolge garantiert wird .

Wenn eine RMW-Operation einen "veralteten" Wert liest, wird garantiert, dass der von ihr erzeugte Schreibvorgang sichtbar ist, bevor Schreibvorgänge von anderen Threads ausgeführt werden, die den gelesenen Wert überschreiben würden.

Update zum Beispiel

Wenn T1 schreibt x=1und T2 x++mit xanfänglich 0 tut , sind die Auswahlmöglichkeiten unter dem Gesichtspunkt der Speicherung von x:

  1. Das Schreiben von T1 erfolgt zuerst, also schreibt T1 x=1, dann liest T2 x==1, erhöht das auf 2 und schreibt es x=2als einzelne atomare Operation zurück.

  2. T1 schreibt an zweiter Stelle. T2 liest x==0, erhöht es auf 1 und schreibt x=1als einzelne Operation zurück, dann schreibt T1 x=1.

Vorausgesetzt, es gibt keine anderen Synchronisationspunkte zwischen diesen beiden Threads, können die Threads mit den Operationen fortfahren, die nicht in den Speicher geleert wurden.

Somit kann T1 ausgeben x=1und dann mit anderen Dingen fortfahren, obwohl T2 immer noch liest x==0(und somit schreibt x=1).

Wenn es andere Synchronisationspunkte gibt, wird ersichtlich, welcher Thread xzuerst geändert wurde , da diese Synchronisationspunkte eine Reihenfolge erzwingen.

Dies ist am offensichtlichsten, wenn Sie eine Bedingung für den aus einer RMW-Operation gelesenen Wert haben.

Update 2

  1. Wenn Sie memory_order_seq_cst(die Standardeinstellung) für alle atomaren Operationen verwenden, müssen Sie sich über solche Dinge keine Gedanken machen. Wenn Sie aus Sicht des Programms "msg" sehen, wurde T1 ausgeführt, dann T3 und dann T2.

Wenn Sie (insbesondere memory_order_relaxed) andere Speicherreihenfolgen verwenden, werden in Ihrem Code möglicherweise andere Szenarien angezeigt.

  1. In diesem Fall haben Sie einen Fehler. Angenommen, das is_enabledFlag ist wahr, wenn T2 in seine whileSchleife eintritt , und entscheidet sich, den Body auszuführen. T1 löscht nun die Daten und T2 verschiebt dann den Zeiger, der ein baumelnder Zeiger ist, und es kommt zu undefiniertem Verhalten . Die Atomics helfen oder behindern in keiner Weise, außer das Datenrennen auf der Flagge zu verhindern.

  2. Sie können einen Mutex mit einer einzelnen atomaren Variablen implementieren.

Anthony Williams
quelle
Vielen Dank @Anthony Wiliams für Ihre schnelle Antwort. Ich habe meine Frage mit einem Beispiel für RMW aktualisiert, das einen "veralteten" Wert liest. Was meinen Sie mit diesem Beispiel unter relativer Reihenfolge und dass das W (1) von T2 vor dem Schreiben sichtbar ist? Bedeutet dies, dass T2, sobald es die Änderungen von T1 gesehen hat, das W (1) von T2 nicht mehr liest?
Albert Caldas
Wenn also "Threads immer veraltete Werte lesen können", bedeutet dies, dass die Cache-Kohärenz niemals garantiert wird (zumindest auf der Ebene des C ++ - Programmierers). Könnten Sie sich bitte mein Update2 ansehen?
Albert Caldas
Jetzt sehe ich, dass ich den Sprach- und Hardware-Speichermodellen mehr Aufmerksamkeit hätte schenken sollen, um all das vollständig zu verstehen, das war das Stück, das mir fehlte. Vielen Dank!
Albert Caldas
1

Bezüglich (3) - hängt es von der verwendeten Speicherreihenfolge ab. Wenn sowohl das Geschäft als auch die RMW-Operation verwendet werden std::memory_order_seq_cst, werden beide Operationen auf irgendeine Weise angeordnet - dh entweder das Geschäft findet vor dem RMW statt oder umgekehrt. Wenn das Geschäft vor dem RMW bestellt wird, wird garantiert, dass die RMW-Operation den gespeicherten Wert "sieht". Wenn das Geschäft nach dem RMW bestellt wird, wird der von der RMW-Operation geschriebene Wert überschrieben.

Wenn Sie entspanntere Speicherreihenfolgen verwenden, werden die Änderungen weiterhin in irgendeiner Weise angeordnet (die Änderungsreihenfolge der Variablen), Sie können jedoch nicht garantieren, ob der RMW den Wert aus der Speicheroperation "sieht" - selbst wenn die RMW-Operation ausgeführt wird ist die Reihenfolge nach dem Schreiben in der Änderungsreihenfolge der Variablen.

Falls Sie noch einen Artikel lesen möchten, verweise ich Sie auf Speichermodelle für C / C ++ - Programmierer .

mpoeter
quelle
Danke für den Artikel, ich hatte ihn noch nicht gelesen. Auch wenn es ziemlich alt ist, war es nützlich, meine Ideen zusammenzustellen.
Albert Caldas
1
Freut mich das zu hören - dieser Artikel ist ein leicht erweitertes und überarbeitetes Kapitel meiner Masterarbeit. :-) Es konzentriert sich auf das Speichermodell, wie es in C ++ 11 eingeführt wurde; Ich könnte es aktualisieren, um die (kleinen) Änderungen widerzuspiegeln, die in C ++ 14/17 eingeführt wurden. Bitte lassen Sie mich wissen, wenn Sie Kommentare oder Verbesserungsvorschläge haben!
mpoeter