2016-07-11 23 views
2

Angenommen, Sie haben eine Eigenschaft public Foo Bar { get; }, die Sie initialisieren möchten. Ein solcher Ansatz könnte darin bestehen, die Klasse Interlocked zu verwenden, die Atomizität für bestimmte Abfolgen von Operationen garantiert (z. B. inkrementieren, hinzufügen, vergleichen, austauschen). Sie könnten dies tun:Ist es richtig, reguläre Lesevorgänge in einem Feld durchzuführen, das von Interlocked.CompareExchange initialisiert wurde?

private Foo _bar; 

public Foo Bar 
{ 
    get 
    { 
     // Initial check, necessary so we don't call new Foo() every time when invoking the method 
     if (_bar == null) 
     { 
      // In the unlikely case 2 threads come in here 
      // at the same time, new Foo() will simply be called 
      // twice, but only one of them will be set to _bar 
      Interlocked.CompareExchange(ref _bar, new Foo(), null); 
     } 
     return _bar; 
    } 
} 

Es gibt viele Orte, die diesen Ansatz zur Lazy-Initialisierung, z. this blog und places in the .NET Framework itself.

Meine Frage ist, sollte das Lesen von _bar nicht flüchtig sein? Zum Beispiel könnte Thread 1 CompareExchange genannt haben und den Wert _bar setzen, aber diese Änderung wäre für Thread 2 nicht sichtbar, da (wenn ich this question richtig verstanden habe) der Wert _bar als null zwischengespeichert wurde, und es könnte enden ruft Interlocked.CompareExchange erneut auf, obwohl Thread 1 bereits _bar eingestellt hat. Sollte also nicht _bar als flüchtig gekennzeichnet werden, um dies zu verhindern?

Kurz gesagt, ist this approach oder this approach zu lazy Initialisierung korrekt? Warum wird Volatile.Read (was hat den gleichen Effekt wie das Markieren einer Variablen volatile und Lesen von ihm) in einem Fall verwendet, aber nicht die andere?

TL bearbeiten; DR: Wenn ein Thread, den Wert eines Feldes über Interlocked.CompareExchange aktualisiert wird, dass aktualisierte Wert zu anderen Threads sofort sichtbar Durchführen einer nichtflüchtigen liest des Feldes?

Antwort

1

Mein erster Gedanke ist "Wen kümmert es?" :)

Was ich meine, ist dies: die Doppel-Check-Initialisierungsmuster ist fast immer übertrieben und es ist leicht zu falsch. Die meiste Zeit ist eine einfache lock die beste: Es ist einfach zu schreiben, performant genug und drückt deutlich die Absicht des Codes aus. Darüber hinaus haben wir jetzt die Lazy<T>-Klasse, um die faule Initialisierung zu abstrahieren, wodurch wir den Code nicht mehr manuell implementieren müssen.

Also, die Besonderheiten des Double-Check-Patterns sind nicht wirklich so wichtig, weil wir es sowieso nicht verwenden sollten.

Das sagte, ich würde mit Ihrer Beobachtung übereinstimmen, dass das Lesen ein flüchtiger gelesen werden sollte. Ohne dies wird die Speicherbarriere, die von Interlocked.CompareExchange() zur Verfügung gestellt wird, nicht unbedingt helfen.

  1. Die Doppel-Karomuster sowieso nicht garantiert ist:

    durch zwei Dinge Dies wird jedoch gemildert. Es gibt eine Racebedingung, selbst wenn Sie einen flüchtigen Lesevorgang haben, daher muss die Initialisierung zwei Mal sicher sein. Selbst wenn der Speicherplatz alt ist, wird nichts wirklich schlimmes passieren. Du wirst den Foo Konstruktor zweimal aufrufen, was nicht großartig ist, aber es kann kein fatales Problem sein, weil das sowieso passieren könnte.

  2. Auf x86 ist der Speicherzugriff standardmäßig flüchtig. Es ist also wirklich nur auf anderen Plattformen, dass dies sowieso ein Problem wird.
+0

Danke für die Antwort! Über den ersten Punkt: Wenn der Thread, der ein reguläres Lesen der Variablen durchführt, dies als null sieht und das Ergebnis zwischenspeichert, wird _bar nicht zurückgegeben und der zwischengespeicherte Wert (null) zurückgegeben, aka return null? (Auch .NET ist auf ARM-Prozessoren, beispielsweise Windows Phone, sehr verbreitet. Deshalb mache ich mir Sorgen.) –

+0

Entschuldigung, können Sie das klären? Nehmen Sie an, dass die Variable registriert ist und so den alten 'null'-Wert zurückgibt, selbst nachdem' CompareExchange() 'aufgerufen wurde? Wenn dem so ist, glaube ich nicht, dass das legal ist. Selbst wenn die Variable registriert ist, muss der Compiler nach dem Aufruf von 'CompareExchange()' den Wert unbedingt erneut lesen, bevor er zurückkehrt, weil 'CompareExchange()' seinen Wert möglicherweise geändert hat. Wenn du das nicht meinst, könntest du genauer sagen, was du meinst? –

+0

Das war was ich meinte. Um also klar zu sein, dass ein Thread den Wert eines Feldes, das von einem anderen Thread mit "CompareExchange" gesetzt wurde, nicht sieht, wenn er "CompareExchange" selbst aufruft, muss er den Wert beim nächsten Lesen erneut aus dem Hauptspeicher laden? –

1

In diesem speziellen Fall nicht-flüchtige Lese tun wird der Code falsch, denn selbst wenn der zweiten Thread verpasste Aktualisierung von _bar es auf CompareExchange nicht machen beobachten wird.Flüchtiges Lesen (möglicherweise) ermöglicht es, den aktualisierten Wert früher zu sehen, ohne dass Schwergewicht erforderlich ist CompareExchange.

In anderen Fällen muss ein Speicherort, der über Interlocked, oder innerhalb von lock geschrieben wurde, als flüchtig gelesen werden.

+0

Vielen Dank für Ihre Antwort! Nur neugierig, da ich mit Threading nicht so erfahren bin, ist der Grund dafür "CompareExchange" (http://stackoverflow.com/a/1716587/4077294), der den zweiten Thread zwingt, die neu zu laden Wert aus dem Speicher oder etwas in dieser Richtung? –

+0

Wenn Sie diese Frage stellen: 'Wenn ein Thread den Wert eines Felds über Interlocked.CompareExchange aktualisiert, wird dieser aktualisierte Wert sofort für andere Threads sichtbar, die nichtflüchtige Lesevorgänge des Feldes ausführen?' Dann nein, 'CompareExchange' macht die Änderung nicht _immediately_ sichtbar für den Code, der von anderen Threads ausgeführt wird. Werte auf Speicherplätzen können auf vielen Ebenen zwischengespeichert werden - es geht nicht nur um CPU. Also auf anderen Threads brauchen Sie eine Barriere, zumindest load-erwerben, die in .NET-Begriffen flüchtig gelesen wird. – OmariO

+0

@JamesKo IMO bequemer und ** praktische ** Modell zu verwenden, wenn mit Speicherzugriffen zu tun ist nicht Caching oder Sichtbarkeit, sondern Speicheroperationen neu anordnen. Werfen Sie einen Blick auf [diesen guten Kommentar] (http://stackoverflow.com/a/17631001/2387098). – OmariO