Frage Die Notwendigkeit für flüchtige Modifizierer in Double Checked Locking in .NET


Mehrere Texte besagen, dass bei der Implementierung der doppelt überprüften Sperrung in .NET das Feld, auf das Sie zugreifen, flüchtig sein 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 notwendige Speicherkonsistenz? Ist es nicht wahr, dass nach der "lock" -Anweisung sowohl Lesen als auch Schreiben flüchtig wären und die notwendige Konsistenz erreicht wäre?


76
2017-12-27 00:13


Ursprung


Antworten:


Flüchtige ist unnötig. Naja, so ungefähr**

volatile wird verwendet, um eine Speicherbarriere * zwischen Lese- und Schreibvorgängen auf der Variablen zu erzeugen.
lockWenn es verwendet wird, werden Speicherbarrieren um den Block in der lockund den Zugriff auf den Block auf einen Thread beschränken.
Speicherbarrieren machen es so, dass jeder Thread den aktuellsten Wert der Variablen (nicht einen lokalen Wert, der in einem Register zwischengespeichert ist) liest und dass der Compiler die Anweisungen nicht neu anordnet. Verwenden volatile ist unnötig **, weil Sie bereits eine Sperre haben.

Joseph Albahari erklärt dieses Zeug viel besser als ich es jemals könnte.

Und schau dir unbedingt Jon Skeets an Anleitung zur Implementierung des Singleton in C #


aktualisieren:
*volatile bewirkt, dass die Variablen gelesen werden VolatileReads und schreibt zu sein VolatileWrites, die auf x86 und x64 auf CLR, sind mit einem implementiert MemoryBarrier. Sie können auf anderen Systemen feinkörniger sein.

** Meine Antwort ist nur korrekt, wenn Sie die CLR auf x86- und x64-Prozessoren verwenden. Es Macht Seien Sie in anderen Speichermodellen wie Mono (und anderen Implementierungen), Itanium64 und zukünftiger Hardware wahr. Darauf bezieht sich Jon in seinem Artikel in den "gotchas" für die doppelte Überprüfung.

Eine von {markieren der Variablen als volatilemit ihm lesen Thread.VolatileReadoder Einfügen eines Anrufs an Thread.MemoryBarrier} ist möglicherweise erforderlich, damit der Code in einer Situation mit einem schwachen Speichermodell ordnungsgemäß funktioniert.

Von dem, was ich verstehe, werden auf der CLR (sogar auf IA64) Schreibvorgänge nie neu geordnet (Schreibvorgänge haben immer eine Freigabesemantik). In IA64 können Lesevorgänge jedoch neu angeordnet werden, um vor Schreibvorgängen zu kommen, es sei denn, sie sind als flüchtig gekennzeichnet. Leider habe ich keinen Zugriff auf die IA64-Hardware, mit der ich spielen könnte. Alles, was ich dazu sage, wäre Spekulation.

Ich habe diese Artikel auch hilfreich gefunden:
http://www.codeproject.com/KB/tips/MemoryBarrier.aspx
Vance Morrisons Artikel (alles verweist darauf, es spricht von doppelt überprüften Sperren)
Chris Brummes Artikel  (alles verweist darauf)
Joe Duffy: Gebrochene Varianten von Double Checked Locking 

luis abreus Serie zum Multithreading gibt einen guten Überblick über die Konzepte
http://msmvps.com/blogs/luisabreu/archive/2009/06/29/multithreading-load-and-store-reordering.aspx
http://msmvps.com/blogs/luisabreu/archive/2009/07/03/multithreading-introducing-memory-fences.aspx 


57
2017-12-27 01:06



Es gibt eine Möglichkeit, es ohne zu implementieren volatileFeld. Ich werde es erklären ...

Ich denke, dass es eine Speicherzugriffsumordnung in der Sperre ist, die gefährlich ist, so dass Sie eine nicht vollständig initialisierte Instanz außerhalb der Sperre 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, dass es im Konstruktor der Singleton-Klasse einige Initialisierungscodes gibt. Wenn diese Anweisungen neu angeordnet werden, nachdem das Feld mit der Adresse des neuen Objekts festgelegt wurde, haben Sie eine unvollständige Instanz ... stellen Sie sich vor, dass die Klasse diesen Code hat:

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 diese Operationen erweitert werden:

ptr = allocate memory for Singleton;
set ptr._value to 1;
set Singleton.instance to ptr;

Was ist, wenn ich diese Anweisungen folgendermaßen neu anordne:

ptr = allocate memory for Singleton;
set Singleton.instance to ptr;
set ptr._value to 1;

Macht es einen Unterschied? NEIN wenn du an einen einzelnen Thread denkst. JA wenn du an mehrere Threads denkst ... 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

Das ist es, was die Speicherbarriere vermeidet, indem sie keine Neuordnung des Speicherzugriffs erlaubt:

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;

Glückliche Kodierung!


31
2017-10-18 00:38



Ich glaube nicht, dass irgendjemand die Frage beantwortet hat FrageAlso 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 würdest du die erste hinzufügen? if (instance == null)?

Der Grund liegt vermutlich darin, die Ausführung des gesperrten Codeabschnitts nicht unnötig zu vermeiden. Während Sie den Code innerhalb der Sperre ausführen, wird jeder andere Thread blockiert, der versucht, diesen Code auszuführen, was Ihr Programm verlangsamt, wenn Sie häufig versuchen, von vielen Threads auf den Singleton zuzugreifen. Abhängig von der Sprache / Plattform kann es auch Overheads von der Sperre selbst geben, die Sie vermeiden möchten.

Daher wird der erste Null-Check als sehr schnelle Möglichkeit hinzugefügt, um zu sehen, ob Sie die Sperre benötigen. Wenn Sie das Singleton nicht erstellen müssen, können Sie die Sperre vollständig vermeiden.

Sie können jedoch nicht überprüfen, ob der Verweis null ist, ohne ihn zu sperren, da ein anderer Thread aufgrund des Prozessor-Cachings diesen ändern könnte und Sie einen "veralteten" Wert lesen würden, der Sie unnötig in die Sperre einbindet. Aber du versuchst, eine Sperre zu vermeiden!

Sie machen also den Singleton flüchtig, um sicherzustellen, dass Sie den letzten Wert lesen, ohne eine Sperre verwenden zu müssen.

Sie benötigen weiterhin die innere Sperre, da flüchtige Objekte Sie nur während eines einzelnen Zugriffs auf die Variable schützen - Sie können sie nicht sicher testen und setzen, ohne eine Sperre zu verwenden.

Nun, ist das wirklich nützlich?

Nun, ich würde sagen "in den meisten Fällen, nein".

Wenn Singleton.Instance aufgrund der Sperren Ineffizienz verursachen könnte, dann Warum nennst du es so oft, dass dies ein bedeutendes Problem wäre?? Der einzige Sinn eines Singletons ist, dass es nur einen gibt, sodass Ihr Code die Singleton-Referenz einmal lesen und zwischenspeichern kann.

Der einzige Fall, den ich mir vorstellen kann, wo diese Zwischenspeicherung nicht möglich wäre, wäre, wenn Sie eine große Anzahl von Threads haben (zB wenn ein Server einen neuen Thread zur Verarbeitung jeder Anfrage verwendet, könnte er Millionen von sehr kurz laufenden Threads erzeugen welches einmal Singleton.Instance aufrufen müsste).

Ich vermute also, dass Double Checked Locking ein Mechanismus ist, der in sehr spezifischen, leistungskritischen Fällen einen echten Platz einnimmt, und dann alle auf den "Dies ist der richtige Weg" gestiegen sind, ohne wirklich zu denken, was es tut und ob es wird tatsächlich notwendig sein, wenn sie es für verwenden.


7
2017-12-27 08:55



AFAIK (und - nimm das mit Vorsicht, ich mache nicht viele gleichzeitige Sachen) nein. Die Sperre gibt Ihnen nur die Synchronisation zwischen mehreren Anwärtern (Threads).

volatile dagegen sagt Ihrer Maschine, den Wert jedes Mal neu zu bewerten, damit Sie nicht auf einen zwischengespeicherten (und falschen) Wert stolpern.

Sehen http://msdn.microsoft.com/en-us/library/ms998558.aspx und notiere das folgende Zitat:

Außerdem wird die Variable als flüchtig deklariert, um sicherzustellen, dass die Zuweisung zur Instanzvariable abgeschlossen ist, bevor auf die Instanzvariable zugegriffen werden kann.

Eine Beschreibung von flüchtigen: http://msdn.microsoft.com/en-us/library/x13ttww7%28VS.71%29.aspx


3
2017-12-27 00:20



Sie sollten volatile mit dem Double Check Lock-Muster verwenden.

Die meisten Leute verweisen auf diesen Artikel als Beweis, dass Sie nicht flüchtig brauchen: https://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10

Aber sie lesen nicht bis zum Ende: "Ein letztes Wort der Warnung - Ich vermute nur das x86-Speichermodell aus beobachtetem Verhalten auf vorhandenen Prozessoren. Low-Lock-Techniken sind daher auch anfällig, da Hardware und Compiler im Laufe der Zeit aggressiver werden können. Hier sind einige Strategien, um die Auswirkungen dieser Fragilität auf Ihren Code zu minimieren. Vermeiden Sie wann immer möglich Low-Lock-Techniken. (...) Stellen Sie sich schließlich das schwächste Speichermodell vor, indem Sie volatile Deklarationen verwenden, anstatt sich auf implizite Garantien zu verlassen. "

Wenn Sie mehr Überzeugungsarbeit benötigen, lesen Sie diesen Artikel zur ECMA-Spezifikation, der 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 eingefügt werden können, die verhindern, dass es ohne flüchtige Bestandteile funktioniert: msdn.microsoft.com/en-us/magazine/jj883956.aspx

Zusammenfassend kann es sein, dass es für Sie ohne Volatilität funktioniert, aber riskieren Sie nicht, dass es den richtigen Code schreibt und entweder volatile oder die volatile_read / write-Methode verwendet. Artikel, die vorschlagen, etwas anderes zu tun, lassen manchmal einige der möglichen Risiken von JIT / Compiler-Optimierungen aus, die sich auf Ihren Code auswirken könnten, sowie auf zukünftige Optimierungen, die Ihren Code brechen könnten. Auch wie bereits im letzten Artikel genannte Annahmen von früheren Annahmen des Arbeitens ohne Volatilität dürfen sich ARM nicht halten.


3
2018-02-23 04:18



Das lock ist ausreichend. Die MS-Sprachspezifikation (3.0) selbst erwähnt dieses genaue Szenario in §8.12 ohne jegliche Erwähnung von volatile:

Ein besserer Ansatz ist die Synchronisierung   Zugriff auf statische Daten durch Sperren von a   privates statisches Objekt. Beispielsweise:

class Cache
{
    private static object synchronizationObject = new object();
    public static void Add(object x) {
        lock (Cache.synchronizationObject) {
          ...
        }
    }
    public static void Remove(object x) {
        lock (Cache.synchronizationObject) {
          ...
        }
    }
}

2
2017-12-27 09:19



Ich denke, dass ich gefunden habe, wonach ich gesucht habe. Details sind in diesem Artikel - http://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10.

Zusammenfassend - in .NET flüchtige Modifikator ist in dieser Situation in der Tat nicht erforderlich. In schwächeren Speichermodellen können Schreibvorgänge, die im Konstruktor von träge initiierten Objekten vorgenommen werden, jedoch nach dem Schreiben in das Feld verzögert werden, sodass andere Threads in der ersten if-Anweisung eine beschädigte Nicht-Null-Instanz lesen können.


2
2017-12-27 22:56



Dies ist ein ziemlich guter Beitrag über die Verwendung von volatile mit Double Checked Locking:

http://tech.puredanger.com/2007/06/15/double-checked-locking/

Wenn in Java eine Variable geschützt werden soll, muss sie nicht gesperrt werden, wenn sie als flüchtig gekennzeichnet ist


-2
2017-12-27 00:19