Verwirrung über Implementierungsfehler in shared_ptr destructor

8

Ich habe gerade Herb Sutter Vortrag gesehen: C ++ und darüber hinaus 2012: Herb Sutter - atomare & lt; & gt; Waffen, 2 von 2

Er zeigt einen Fehler bei der Implementierung von std :: shared_ptr destructor:

%Vor%

Er sagt, dass memory_order_relaxed gelöscht werden kann, bevor fetch_sub.

  

Um 1:25:18 - Release hält Zeile B nicht unten, wo es sein sollte

Wie ist das möglich? Vor der Beziehung gibt es eine Situation vor / sequenziert, weil sie beide in einem einzelnen Thread sind. Ich könnte mich irren, aber es gibt auch eine Abhängigkeit zwischen fetch_sub und delete.

Wenn er Recht hat, welche ISO-Elemente unterstützen das?

    
qble 14.02.2013, 17:54
quelle

4 Antworten

2

Stellen Sie sich einen Code vor, der einen gemeinsamen Zeiger freigibt:

%Vor%

Wenn dec_ref () keine "release" -Semantik hat, ist es völlig in Ordnung, wenn ein Compiler (oder eine CPU) Dinge von vor dec_ref () zu nachher (zB) bewegt:

%Vor%

Und das ist nicht sicher, da dec_ref () auch von anderen Threads gleichzeitig aufgerufen werden kann und das Objekt löscht. Also muss es eine "release" -Semantik für Dinge vor dec_ref () haben, um dort zu bleiben.

Stellen wir uns nun vor, dass der Destruktor des Objekts so aussieht:

%Vor%

Auch wir werden das Beispiel ein bisschen ändern und werden 2 Threads haben:

%Vor%

Dann wird der "aggregierte" Code wie folgt aussehen:

%Vor%

Wenn wir jedoch nur eine "release" -Semantik für atomic_sub () haben, kann dieser Code auf folgende Weise optimiert werden:

%Vor%

Aber so wird der Destruktor nicht immer den letzten Wert von "a" ausgeben (dieser Code ist nicht mehr rennfrei). Deshalb brauchen wir auch eine Semantik für atomic_sub (oder genau genommen brauchen wir eine Acquir-Barriere, wenn der Zähler nach Dekrement 0 wird).

    
wonder.mice 10.02.2015 01:07
quelle
0

Im Gespräch zeigt Herb memory_order_release nicht memory_order_relaxed , aber entspannt hätte noch mehr Probleme.

Wenn delete control_block_ptr auf control_block_ptr->refs zugreift (was wahrscheinlich nicht der Fall ist), führt die atomare Operation keine Abhängigkeit zum Löschen. Die Löschoperation berührt möglicherweise keinen Speicher im Steuerblock, sondern gibt den Zeiger nur an den Freestore-Allokator zurück.

Aber ich bin mir nicht sicher, ob Herb darüber spricht, dass der Compiler das Löschen vor der atomaren Operation verschiebt oder sich nur darauf bezieht, wenn die Nebenwirkungen für andere Threads sichtbar werden.

    
Jonathan Wakely 14.02.2013 18:53
quelle
0

Sieht aus, als würde er über die Synchronisierung von Aktionen auf dem gemeinsamen Objekt selbst sprechen, die nicht auf seinen Code-Blöcken angezeigt werden (und als Ergebnis - verwirrend).

Deshalb hat er acq_rel gesetzt - weil alle Aktionen auf dem Objekt vor seiner Zerstörung stattfinden sollen, alles in der richtigen Reihenfolge.

Aber ich bin mir immer noch nicht sicher, warum er über den Austausch von delete mit fetch_sub redet.

    
qble 14.02.2013 19:23
quelle
0

Dies ist eine späte Antwort.

Beginnen wir mit diesem einfachen Typ:

%Vor%

Und wir verwenden diesen Typ in shared_ptr wie folgt:

%Vor%

Zwei Threads werden parallel ausgeführt, wobei beide den Besitz eines foo -Objekts teilen.

Mit einer korrekten shared_ptr Implementierung (also eins mit memory_order_acq_rel ), dieses Programm hat ein definiertes Verhalten. Der einzige Wert, der von diesem Programm ausgegeben wird, ist 5 .

Bei einer falschen Implementierung (mit memory_order_relaxed ) sind keine solchen Garantien. Das Verhalten ist nicht definiert, da ein Datenrennen von foo::value wird eingeführt. Das Problem tritt nur für Fälle auf, wenn der Destruktor wird im Hauptthread aufgerufen. Mit einer entspannten Speicherordnung, dem Schreiben to foo::value im anderen Thread wird möglicherweise nicht an den Destruktor im Hauptthread weitergegeben. Ein anderer Wert als 5 könnte gedruckt werden.

Was ist ein Datenrennen? Nun, schauen Sie sich die Definition an und achten Sie auf den letzten Punkt:

  

Wenn eine Auswertung eines Ausdrucks in einen Speicherort schreibt und eine andere Auswertung den gleichen Speicherort liest oder ändert, wird gesagt, dass die Ausdrücke in Konflikt stehen. Ein Programm, das zwei widersprüchliche Bewertungen hat, hat ein Datenrennen, es sei denn

     
  • beide widersprüchlichen Bewertungen sind atomare Operationen (siehe std :: atomic)
  •   
  • eine der widersprüchlichen Auswertungen passiert - vor einer anderen (siehe std :: memory_order)
  •   

In unserem Programm schreibt ein Thread in foo::value und ein Thread wird Lese von foo::value . Diese sollen sequentiell sein; das Schreiben to foo::value sollte immer vor dem Lesen passieren. Intuitiv macht Sinn, dass sie so sein würden, wie der Destruktor der letzte sein soll was passiert mit einem Objekt.

memory_order_relaxed bietet jedoch keine solchen Bestellgarantien und daher wird memory_order_acq_rel benötigt.

    
Pubby 07.07.2016 06:57
quelle