In mehreren Texten heißt es, dass bei der Implementierung einer doppelt aktivierten Sperre in .NET auf das Feld, auf das Sie sperren, ein flüchtiger Modifikator angewendet werden sollte. Aber warum genau? Betrachtet man das folgende Beispiel:
public sealed class Singleton
{
private static volatile Singleton instance;
private static object syncRoot = new Object();
private Singleton() {}
public static Singleton Instance
{
get
{
if (instance == null)
{
lock (syncRoot)
{
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
}
Warum erreicht "lock (syncRoot)" nicht die erforderliche Speicherkonsistenz? Stimmt es nicht, dass nach der "Lock" -Anweisung sowohl Lesen als auch Schreiben flüchtig sind und somit die erforderliche Konsistenz erreicht wird?
Antworten:
Flüchtig ist nicht erforderlich. Naja, so ungefähr**
volatile
wird verwendet, um eine Speicherbarriere * zwischen Lese- und Schreibvorgängen für die Variable zu erstellen.lock
Bei Verwendung werden Speicherbarrieren um den Block innerhalb des Blocks erstellt und derlock
Zugriff auf den Block auf einen Thread beschränkt.Speicherbarrieren sorgen dafür, dass jeder Thread den aktuellsten Wert der Variablen liest (kein lokaler Wert, der in einem Register zwischengespeichert ist) und dass der Compiler die Anweisungen nicht neu anordnet. Die Verwendung
volatile
ist nicht erforderlich **, da Sie bereits eine Sperre haben.Joseph Albahari erklärt dieses Zeug viel besser als ich es jemals könnte.
Lesen Sie auch Jon Skeets Leitfaden zur Implementierung des Singletons in C #
update :
*
volatile
bewirkt, dass Lesevorgänge der VariablenVolatileRead
s und SchreibvorgängeVolatileWrite
s sind, die unter x86 und x64 unter CLR mit a implementiert sindMemoryBarrier
. Sie können auf anderen Systemen feinkörniger sein.** Meine Antwort ist nur richtig, wenn Sie die CLR auf x86- und x64-Prozessoren verwenden. Es könnte in anderen Speichermodellen, wie auf Mono (und anderen Implementierungen), Itanium64 und zukünftige Hardware wahr sein. Darauf bezieht sich Jon in seinem Artikel in den "Fallstricken" für doppelt geprüftes Sperren.
Möglicherweise ist es erforderlich , die Variable als {Markieren der Variablen als
volatile
, Lesen mitThread.VolatileRead
oder Einfügen eines Aufrufs anThread.MemoryBarrier
} auszuführen, damit der Code in einer schwachen Speichermodellsituation ordnungsgemäß funktioniert.Soweit ich weiß, werden Schreibvorgänge in der CLR (auch auf IA64) nie neu angeordnet (Schreibvorgänge haben immer eine Release-Semantik). Auf IA64 können Lesevorgänge jedoch vor dem Schreiben neu angeordnet werden, es sei denn, sie sind als flüchtig gekennzeichnet. Leider habe ich keinen Zugriff auf IA64-Hardware, mit der ich spielen kann. Alles, was ich dazu sage, wäre Spekulation.
Ich fand diese Artikel auch hilfreich:
http://www.codeproject.com/KB/tips/MemoryBarrier.aspx
vance morrisons artikel (alles verlinkt darauf, es geht um doppelt geprüftes sperren)
chris brummes artikel (alles verlinkt darauf )
Joe Duffy: Defekte Varianten der doppelt überprüften Verriegelung
luis abreus serie zum multithreading gibt auch einen schönen überblick über die konzepte
http://msmvps.com/blogs/luisabreu/archive/2009/06/29/multithreading-load-and-store-reordering.aspx
http: // msmvps. com / Blogs / Luisabreu / Archiv / 2009/07/03 / Multithreading-Einführung-Memory-Fences.aspx
quelle
volatile
auf unnötig war jede Plattform dann würde es bedeuten , die JIT nicht die Speicherlasten optimieren könnte ,object s1 = syncRoot; object s2 = syncRoot;
umobject s1 = syncRoot; object s2 = s1;
auf dieser Plattform. Das scheint mir sehr unwahrscheinlich.Es gibt eine Möglichkeit, es ohne
volatile
Feld zu implementieren . Ich werde es erklären ...Ich denke, dass die Neuordnung des Speicherzugriffs innerhalb des Schlosses gefährlich ist, sodass Sie eine nicht vollständig initialisierte Instanz außerhalb des Schlosses erhalten können. Um dies zu vermeiden, mache ich Folgendes:
public sealed class Singleton { private static Singleton instance; private static object syncRoot = new Object(); private Singleton() {} public static Singleton Instance { get { // very fast test, without implicit memory barriers or locks if (instance == null) { lock (syncRoot) { if (instance == null) { var temp = new Singleton(); // ensures that the instance is well initialized, // and only then, it assigns the static variable. System.Threading.Thread.MemoryBarrier(); instance = temp; } } } return instance; } } }
Den Code verstehen
Stellen Sie sich vor, der Konstruktor der Singleton-Klasse enthält einen Initialisierungscode. Wenn diese Anweisungen neu angeordnet werden, nachdem das Feld mit der Adresse des neuen Objekts festgelegt wurde, liegt eine unvollständige Instanz vor. Stellen Sie sich vor, die Klasse hat diesen Code:
private int _value; public int Value { get { return this._value; } } private Singleton() { this._value = 1; }
Stellen Sie sich nun einen Aufruf des Konstruktors mit dem neuen Operator vor:
instance = new Singleton();
Dies kann auf folgende Operationen erweitert werden:
ptr = allocate memory for Singleton; set ptr._value to 1; set Singleton.instance to ptr;
Was ist, wenn ich diese Anweisungen wie folgt nachbestelle:
ptr = allocate memory for Singleton; set Singleton.instance to ptr; set ptr._value to 1;
Macht es einen Unterschied? NEIN, wenn Sie an einen einzelnen Thread denken. JA, wenn Sie an mehrere Threads denken ... was ist, wenn der Thread kurz danach unterbrochen wird
set instance to ptr
:ptr = allocate memory for Singleton; set Singleton.instance to ptr; -- thread interruped here, this can happen inside a lock -- set ptr._value to 1; -- Singleton.instance is not completelly initialized
Dies wird durch die Speicherbarriere vermieden, indem keine Neuordnung des Speicherzugriffs zugelassen wird:
ptr = allocate memory for Singleton; set temp to ptr; // temp is a local variable (that is important) set ptr._value to 1; -- memory barrier... cannot reorder writes after this point, or reads before it -- -- Singleton.instance is still null -- set Singleton.instance to temp;
Viel Spaß beim Codieren!
quelle
new
vollständig initialisiert ..Ich glaube nicht, dass jemand die Frage tatsächlich beantwortet hat , also werde ich es versuchen.
Das Flüchtige und das Erste
if (instance == null)
sind nicht "notwendig". Die Sperre macht diesen Code threadsicher.Die Frage ist also: Warum sollten Sie die erste hinzufügen
if (instance == null)
?Der Grund ist vermutlich, zu vermeiden, dass der gesperrte Codeabschnitt unnötig ausgeführt wird. Während Sie den Code innerhalb der Sperre ausführen, wird jeder andere Thread blockiert, der versucht, diesen Code ebenfalls auszuführen. Dies verlangsamt Ihr Programm, wenn Sie versuchen, häufig von vielen Threads aus auf den Singleton zuzugreifen. Abhängig von der Sprache / Plattform kann es auch zu Overheads des Schlosses kommen, die Sie vermeiden möchten.
Die erste Nullprüfung wird also hinzugefügt, um schnell festzustellen, ob Sie die Sperre benötigen. Wenn Sie den Singleton nicht erstellen müssen, können Sie die Sperre vollständig umgehen.
Sie können jedoch nicht überprüfen, ob die Referenz null ist, ohne sie auf irgendeine Weise zu sperren, da sie aufgrund des Prozessor-Caching von einem anderen Thread geändert werden könnte und Sie einen "veralteten" Wert lesen würden, der dazu führen würde, dass Sie die Sperre unnötig betreten. Aber Sie versuchen, eine Sperre zu vermeiden!
Sie machen den Singleton also flüchtig, um sicherzustellen, dass Sie den neuesten Wert lesen, ohne eine Sperre verwenden zu müssen.
Sie benötigen weiterhin das innere Schloss, da flüchtig Sie nur während eines einzelnen Zugriffs auf die Variable schützt - Sie können es nicht sicher testen und einstellen, ohne ein Schloss zu verwenden.
Ist das wirklich nützlich?
Nun, ich würde sagen "in den meisten Fällen nein".
Wenn Singleton.Instance aufgrund der Sperren zu Ineffizienz führen kann, warum rufen Sie es dann so häufig auf, dass dies ein erhebliches Problem darstellt ? Der springende Punkt eines Singletons ist, dass es nur einen gibt, sodass Ihr Code die Singleton-Referenz einmal lesen und zwischenspeichern kann.
Der einzige Fall, in dem ich mir vorstellen kann, wo dieses Caching nicht möglich wäre, wäre, wenn Sie eine große Anzahl von Threads haben (z. B. könnte ein Server, der einen neuen Thread zum Verarbeiten jeder Anforderung verwendet, Millionen von sehr kurz laufenden Threads erstellen, von denen jeder die Singleton.Instance einmal aufrufen müsste).
Ich vermute also, dass doppelt geprüftes Sperren ein Mechanismus ist, der in ganz bestimmten leistungskritischen Fällen einen echten Platz hat, und dann sind alle auf den Zug "Dies ist der richtige Weg, dies zu tun" geklettert, ohne wirklich darüber nachzudenken, was er tut und ob er es tut wird tatsächlich notwendig sein, wenn sie es für verwenden.
quelle
volatile
hat nichts mit der Sperrsemantik beim doppelt überprüften Sperren zu tun, sondern mit dem Speichermodell und der Cache-Kohärenz. Damit soll sichergestellt werden, dass ein Thread keinen Wert erhält, der noch von einem anderen Thread initialisiert wird, was durch das Double-Check-Sperrmuster nicht von Natur aus verhindert wird. In Java benötigen Sie auf jeden Fall dasvolatile
Schlüsselwort. In .NET ist es trübe, weil es laut ECMA falsch ist, aber gemäß der Laufzeit richtig. In jedemlock
Fall kümmert sich das definitiv nicht darum.lock
der Code threadsicher ist. Dieser Teil ist wahr, aber das Double-Check-Sperrmuster kann ihn unsicher machen . Das scheint dir zu fehlen. Diese Antwort scheint sich über die Bedeutung und den Zweck eines Double-Check-Schlosses zu schlängeln, ohne jemals die Thread-Sicherheitsprobleme anzusprechen, die der Grund dafür sindvolatile
.instance
es mit markiert istvolatile
?Sie sollten flüchtig mit dem Double-Check-Lock-Muster verwenden.
Die meisten Leute verweisen auf diesen Artikel als Beweis dafür, dass Sie keine flüchtigen Bestandteile benötigen: https://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10
Sie lesen jedoch nicht bis zum Ende: " Ein letztes Wort der Warnung - Ich vermute nur das x86-Speichermodell aufgrund des beobachteten Verhaltens auf vorhandenen Prozessoren. Daher sind Low-Lock-Techniken auch fragil, da Hardware und Compiler mit der Zeit aggressiver werden können Hier sind einige Strategien, um die Auswirkungen dieser Fragilität auf Ihren Code zu minimieren. Vermeiden Sie nach Möglichkeit Low-Lock-Techniken. (...) Nehmen Sie schließlich das schwächste Speichermodell an, indem Sie flüchtige Deklarationen verwenden, anstatt sich auf implizite Garantien zu verlassen . "
Wenn Sie mehr Überzeugungsarbeit benötigen, lesen Sie diesen Artikel über die ECMA-Spezifikation, die für andere Plattformen verwendet wird: msdn.microsoft.com/en-us/magazine/jj863136.aspx
Wenn Sie weitere Überzeugungsarbeit benötigen, lesen Sie diesen neueren Artikel, in dem Optimierungen vorgenommen werden können, die verhindern, dass er ohne Flüchtigkeit funktioniert: msdn.microsoft.com/en-us/magazine/jj883956.aspx
Zusammenfassend lässt sich sagen, dass es für Sie "möglicherweise" funktioniert, ohne dass es im Moment flüchtig ist, aber es ist nicht wahrscheinlich, dass es den richtigen Code schreibt und entweder die flüchtigen oder die volatileread / write-Methoden verwendet. Artikel, die eine andere Vorgehensweise vorschlagen, lassen manchmal einige der möglichen Risiken von JIT / Compiler-Optimierungen aus, die sich auf Ihren Code auswirken könnten, sowie uns zukünftige Optimierungen, die möglicherweise Ihren Code beschädigen könnten. Wie bereits im letzten Artikel erwähnt, gelten frühere Annahmen, ohne Volatilität zu arbeiten, möglicherweise nicht für ARM.
quelle
AFAIK (und - seien Sie vorsichtig, ich mache nicht viele gleichzeitige Sachen) nein. Die Sperre gibt Ihnen nur die Synchronisation zwischen mehreren Konkurrenten (Threads).
volatile hingegen weist Ihre Maschine an, den Wert jedes Mal neu zu bewerten, damit Sie nicht auf einen zwischengespeicherten (und falschen) Wert stoßen.
Siehe http://msdn.microsoft.com/en-us/library/ms998558.aspx und beachten Sie das folgende Zitat:
Eine Beschreibung von flüchtig: http://msdn.microsoft.com/en-us/library/x13ttww7%28VS.71%29.aspx
quelle
Ich glaube, ich habe gefunden, wonach ich gesucht habe. Details finden Sie in diesem Artikel - http://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10 .
Zusammenfassend ist in dieser Situation ein flüchtiger Modifikator in .NET in der Tat nicht erforderlich. In schwächeren Speichermodellen können Schreibvorgänge im Konstruktor eines träge initiierten Objekts jedoch nach dem Schreiben in das Feld verzögert werden, sodass andere Threads möglicherweise eine beschädigte Nicht-Null-Instanz in der ersten if-Anweisung lesen.
quelle
Das
lock
reicht aus. Die MS-Sprachspezifikation (3.0) selbst erwähnt dieses genaue Szenario in §8.12, ohne Folgendes zu erwähnenvolatile
:quelle
Dies ist ein ziemlich guter Beitrag über die Verwendung von volatile mit doppelt überprüfter Verriegelung:
http://tech.puredanger.com/2007/06/15/double-checked-locking/
Wenn in Java eine Variable geschützt werden soll, müssen Sie sie nicht sperren, wenn sie als flüchtig markiert ist
quelle