Warum wird eine Acquir-Barriere benötigt, bevor die Daten in einem atomar referenzierten Smart-Zeiger gelöscht werden?

8

Boost bietet einen Sample-Verweis auf Atomic Reference Geared Pointer

Hier ist das relevante Code-Snippet und die Erklärung für die verschiedenen verwendeten Anordnungen:

%Vor%
  

Das Erhöhen des Referenzzählers kann immer mit durchgeführt werden   memory_order_relaxed: Neue Referenzen auf ein Objekt können nur gebildet werden   aus einer bestehenden Referenz und eine vorhandene Referenz von einer übergeben   Thread zu einem anderen muss bereits eine erforderliche Synchronisierung bereitstellen.

     

Es ist wichtig, jeden möglichen Zugriff auf das Objekt in einem zu erzwingen   Thread (über eine vorhandene Referenz) vor dem Löschen der   Objekt in einem anderen Thread. Dies wird durch eine "Freigabe" erreicht   Operation nach dem Löschen einer Referenz (beliebiger Zugriff auf das Objekt durch   Diese Referenz muss offensichtlich vorher geschehen), und ein "erwerben"   Operation vor dem Löschen des Objekts.

     

Es wäre möglich, memory_order_acq_rel für den fetch_sub zu verwenden   Operation, aber dies führt zu unnötigen "akquirieren" Operationen, wenn die   Referenzzähler erreicht noch nicht Null und kann eine Leistung auferlegen   Strafe.

Ich kann nicht verstehen, warum die Barriere memory_order_acquire vor der Operation delete x notwendig ist. Insbesondere, wie ist es für den Compiler / Prozessor sicher, die Speicheroperationen von delete x vor dem fetch_sub und den Test vom Wert von x == 1 neu zu ordnen, ohne die Semantik mit einem einzigen Thread zu verletzen?

BEARBEITEN Ich denke, meine Frage war nicht sehr klar. Hier ist eine umformulierte Version:

Liefert die Kontrollabhängigkeit zwischen dem Lesen von x ( x->refcount_.fetch_sub(1, boost::memory_order_release) == 1 ) und der delete x -Operation überhaupt eine Bestellgarantie? Ist es möglich, dass der Compiler / Prozessor die Befehle, die der delete x -Operation vor der fetch_sub und dem Vergleich entsprechen, neu anordnet, selbst wenn man ein single-threaded-Programm betrachtet. Es wäre wirklich hilfreich, wenn die Antwort so niedrig wie möglich wäre und ein Beispielszenario enthält, in dem die Löschoperation neu geordnet wird (ohne die Singlethread-Semantik zu beeinflussen), was zeigt, dass die Reihenfolge beibehalten werden muss.

    
Scott Peterson 03.01.2015, 02:05
quelle

2 Antworten

5

Betrachten Sie zwei Threads, die jeweils einen Verweis auf das Objekt enthalten, die die letzten beiden Referenzen sind:

%Vor%

Sie müssen sicherstellen, dass alle Änderungen, die an dem Objekt von Thread 1 in //play with x here vorgenommen wurden, für Thread 2 sichtbar sind, wenn es delete x; aufruft. Dazu benötigen Sie einen Acquired Fence, der zusammen mit den memory_order_release auf den fetch_sub() Aufrufen garantiert, dass die von Thread 1 vorgenommenen Änderungen sichtbar sind.

    
T.C. 03.01.2015 03:13
quelle
2

Von Ссылка

  

memory_order_acquire - Eine Ladeoperation mit dieser Speicherreihenfolge führt die Erfassungsoperation an dem betroffenen Speicherort durch: prior   schreibt an anderen Speicherorten nach dem Thread , der das gemacht hat   Release wird in diesem Thread sichtbar.

     

...

     

Freigabe-Erwerben der Bestellung

     

Wenn ein atomarer Speicher in Thread A mit std :: memory_order_release und   eine atomare Ladung in Thread B aus derselben Variablen wird markiert   std :: memory_order_acquire, alle Speicher schreibt (nicht atomar und entspannt   Atomar), die vor dem Atomladen aus der Sicht passiert ist   von Thread A, werden sichtbare Nebenwirkungen in Thread B, das heißt, einmal   Die atomare Ladung ist abgeschlossen, Thread B sieht garantiert alles   Thread A schrieb in den Speicher.

     

Die Synchronisation wird nur zwischen den freigebenden Threads eingerichtet   und Erlangen derselben atomaren Variable. Andere Threads können sehen   unterschiedliche Reihenfolge der Speicherzugriffe als eines oder beide der   synchronisierte Threads.

     

Auf stark geordneten Systemen (x86, SPARC TSO, IBM Mainframe)   Die Bestellung von release-aware erfolgt automatisch für die meisten Vorgänge.   Für diese Synchronisation werden keine zusätzlichen CPU-Anweisungen ausgegeben   Modus sind nur bestimmte Compileroptimierungen betroffen (z. B. der   Compiler ist es nicht erlaubt, nicht-atomare Speicher hinter dem Atom zu bewegen   Speichern-release oder führen Sie nicht-atomare Lasten früher als das atomare   laden-erwerben). Auf schwach geordneten Systemen (ARM, Itanium, PowerPC)   spezielle CPU-Lade- oder Memory-Fence-Anweisungen müssen verwendet werden.

Dies bedeutet, dass release es anderen Threads erlaubt, ausstehende Operationen des aktuellen Threads zu synchronisieren, während das spätere acquire alle modifizierten Änderungen von den anderen Threads abruft.

Bei stark geordneten Systemen ist dies nicht so wichtig. Ich glaube nicht, dass diese Befehle sogar Code erzeugen, da die CPU Cache-Zeilen automatisch sperrt, bevor irgendwelche Schreibvorgänge auftreten können. Der Cache ist garantiert konsistent. Aber auf wöchentlich geordneten Systemen, während atomare Operationen wohldefiniert sind, könnten Operationen an anderen Teilen des Speichers ausstehen.

Sagen wir also die Threads A und B, und beide teilen einige Daten D.

  1. A bekommt ein Schloss und es macht Dinge mit D
  2. Eine Freigabesperre
  3. B gibt die Sperre frei, findet 0 ref count und entscheidet sich, D
  4. zu löschen
  5. löscht D
  6. ... die in # 1 anstehenden Daten sind noch nicht sichtbar, so dass schlimme Dinge passieren.

Wenn der Thread-Fence vor dem Löschen abgerufen wird, synchronisiert der aktuelle Thread alle ausstehenden Operationen von anderen Threads in seinem Adressraum. Und wenn Löschen passiert, sieht er, was A in # 1 getan hat.

    
user3427419 03.01.2015 03:19
quelle