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:
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:
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:
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?
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.
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.
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.