Neuordnung von Operationen in der Umgebung von volatile

8

Ich schaue mir gerade eine Implementierung des Copy-on-Write-Sets an und möchte bestätigen, dass es Thread-sicher ist. Ich bin ziemlich sicher, dass der einzige Weg, es möglicherweise nicht ist, wenn der Compiler Anweisungen in bestimmten Methoden neu anordnen kann. Zum Beispiel sieht die Methode Remove folgendermaßen aus:

%Vor%

Dabei ist hashSet als

definiert %Vor%

Also meine Frage ist, da hashSet volatile bedeutet, dass die Remove auf der neuen Menge vor dem Schreiben auf die Member-Variable passiert? Wenn nicht, dann können andere Threads die Menge sehen, bevor die Entfernung aufgetreten ist.

Ich habe in der Produktion keine Probleme gesehen, aber ich möchte nur bestätigen, dass es sicher ist.

AKTUALISIEREN

Um genauer zu sein, gibt es eine andere Methode, um ein IEnumerator zu erhalten:

%Vor%

Also ist die spezifischere Frage: Gibt es eine Garantie, dass die zurückgegebene IEnumerator niemals eine ConcurrentModificationException von einer Entfernung werfen wird?

UPDATE 2

Entschuldigung, die Antworten beziehen sich alle auf die Threadsicherheit von mehreren Writern. Gute Punkte werden angesprochen, aber das versuche ich hier nicht herauszufinden. Ich würde gerne wissen, ob der Compiler die Operationen in Remove auf etwas wie diese umordnen kann:

%Vor%

Wenn dies möglich wäre, würde ein Thread GetEnumerator aufrufen, nachdem hashSet zugewiesen wurde, aber bevor item entfernt wurde, was dazu führen könnte, dass die Sammlung geändert wird während der Aufzählung.

Joe Duffy hat einen Blog-Artikel , der besagt:

  

Flüchtig auf Lasten bedeutet, nicht mehr, nicht weniger. (Es gibt   zusätzliche Compiler-Optimierungseinschränkungen natürlich nicht   das Hissen außerhalb von Schleifen erlauben, aber konzentrieren wir uns auf die MM-Aspekte   für jetzt.) Die Standarddefinition von ACQUIRE ist die folgende   Speicheroperationen können sich nicht vor dem ACQUIRE-Befehl bewegen; z.B.   gegeben durch {ld.acq X, ld Y}, kann das ld Y nicht vor ld.acq X auftreten.   Vorherige Speicheroperationen können sich jedoch sicher danach bewegen; z.B.   gegeben {ld X, ld.acq Y}, kann das ld.acq Y tatsächlich vor dem ld auftreten   X. Der einzige Prozessor, für den derzeit Microsoft .NET-Code ausgeführt wird   Dies ist tatsächlich IA64, aber dies ist ein bemerkenswerter Bereich, wo CLRs   MM ist schwächer als die meisten Maschinen. Als nächstes sind alle Geschäfte auf .NET RELEASE   (unabhängig von volatil, d. h. flüchtig ist ein No-Op in Bezug auf Jitted   Code). Die Standarddefinition von RELEASE ist der vorherige Speicher   Operationen können sich nach einer RELEASE-Operation nicht bewegen; z.B. gegeben {st X,   st.rel Y}, das st.rel Y kann nicht vor st X vorkommen.   nachfolgende Speicheroperationen können sich tatsächlich davor bewegen; z.B. gegeben {   st.rel X, ld Y}, kann sich das ld Y vor st.rel X bewegen.

Ich lese das so, dass der Aufruf von newHashSet.Remove ein ld newHashSet erfordert und das Schreiben von hashSet ein st.rel newHashSet erfordert. Aus der obigen Definition von RELEASE können keine Lasten nach dem Geschäft RELEASE verschoben werden, also die Anweisungen können nicht neu geordnet werden ! Könnte jemand bestätigen bitte bestätigen, dass meine Interpretation korrekt ist?

    
SimonC 06.07.2012, 07:07
quelle

3 Antworten

1

BEARBEITET:

Vielen Dank für die Klärung des Vorhandenseins einer externen Sperre für Aufrufe zum Entfernen (und anderer Auflistungsmutationen).

Wegen der RELEASE-Semantik speichern Sie keinen neuen Wert in hashSet , bis nachdem der Wert für die Variable removed zugewiesen wurde (weil st removed nicht verschoben werden kann) nach st.rel hashSet ).

Daher funktioniert das 'Schnappschuss'-Verhalten von GetEnumerator wie vorgesehen, zumindest in Bezug auf Remove und andere Mutatoren, die auf ähnliche Weise implementiert werden.

    
Monroe Thomas 10.07.2012, 19:39
quelle
3

Erwägen Sie die Verwendung von Interlocked.Exchange - es garantiert die Reihenfolge oder Interlocked.CompareExchange , mit dem Sie simultane Schreibvorgänge für die Sammlung erkennen und möglicherweise wiederherstellen können. Es fügt eindeutig eine zusätzliche Synchronisationsstufe hinzu, so dass es sich von Ihrem aktuellen Code unterscheidet, aber einfacher zu verstehen ist.

%Vor%

Und ich denke, dass du in diesem Fall volatile hashSet brauchst.

    
Alexei Levenkov 06.07.2012 07:53
quelle
0

Ich kann nicht für C # sprechen, aber in C zeigt volatile im Prinzip an und zeigt nur an, dass sich der Inhalt der Variablen jederzeit ändern kann. Es bietet keine Einschränkungen hinsichtlich der Neuordnung von Compilern oder CPUs. Alles, was Sie bekommen, ist, dass der Compiler / CPU den Wert immer aus dem Speicher liest, anstatt einer zwischengespeicherten Version zu vertrauen.

Ich glaube jedoch, dass in letzter Zeit MSVC (und so möglicherweise C #) das Lesen eines flüchtigen Speichers als eine Speicherbarriere für Lasten wirkt und das Schreiben als eine Speicherbarriere für Speicher, z.B. Die CPU wird angehalten, bis alle Lasten vollendet sind und keine Lasten entkommen können, indem sie unter dem flüchtigen Lesen neu geordnet werden (obwohl spätere unabhängige Lasten sich immer noch über die Speicherschranke hinaus bewegen können); und die CPU wird stehen bleiben, bis Speicher abgeschlossen sind, und keine Speicher können diesem entgehen, indem sie unterhalb des flüchtigen Schreibvorgangs neu geordnet werden (obwohl spätere unabhängige Schreibvorgänge sich immer noch über die Speicherschranke hinaus bewegen können).

Wenn nur ein einzelner Thread eine gegebene Variable schreibt (aber viele lesen), sind nur Speicherbarrieren für den korrekten Betrieb erforderlich. Wenn mehrere Threads auf eine bestimmte Variable schreiben, müssen atomare Operationen verwendet werden, da das CPU-Design so ist, dass es beim Schreiben grundsätzlich eine Race-Bedingung gibt, so dass ein Schreiben verloren gehen kann.

    
user82238 06.07.2012 09:16
quelle

Tags und Links