Ist eine Sperre mit einer faulen Initialisierung auf einem tief unveränderlichen Typ erforderlich?

StackOverflow https://stackoverflow.com/questions/652195

Frage

Wenn ich eine tief unveränderlichen Typ (alle Mitglieder sind nur lesbar und wenn sie Referenzart Mitglieder sind, dann auf Objekte, die sie beziehen sich auch die tief unveränderlich sind).

Ich möchte einen faulen initialisiert Eigenschaft von der Art implementieren, wie folgt aus:

private ReadOnlyCollection<SomeImmutableType> m_PropName = null;
public ReadOnlyCollection<SomeImmutableType> PropName
{
    get
    {
        if(null == m_PropName)
        {
            ReadOnlyCollection<SomeImmutableType> temp = /* do lazy init */;
            m_PropName = temp;
        }
        return m_PropName;
    }
}

Von dem, was ich sagen kann:

m_PropName = temp; 

... ist THREAD. Ich mache mir keine Sorgen zu viel über zwei Threads beide Rennen in der gleichen Zeit zu initialisieren, weil es selten sein wird, beide Ergebnisse aus einer logischen Perspektive identisch sein würde, und ich möchte lieber keine Sperre verwenden, wenn ich nicht habe zu.

Wird diese Arbeit? Was sind die Vor- und Nachteile?

Edit: Danke für deine Antworten. Ich werde wahrscheinlich mit der Verwendung einer Sperre vorwärts bewegen. Allerdings bin ich brachte überrascht niemand die Möglichkeit der Compiler die Realisierung bis dass die TEMP-Variable nicht notwendig ist, und nur die Zuordnung direkt zu m_PropName. Wenn das der Fall wäre, dann könnte ein Lese Thread möglicherweise ein Objekt gelesen, die konstruiert ist noch nicht abgeschlossen ist. Hat der Compiler verhindern eine solche Situation?

(Antworten scheinen zu zeigen, dass die Laufzeit wird dies nicht zulassen.)

Edit: Also habe ich beschlossen, mit einer Sperrungs CompareExchange Methode von diesem Artikel inspiriert gehen durch Joe Duffy .

Grundsätzlich gilt:

private ReadOnlyCollection<SomeImmutableType> m_PropName = null;
public ReadOnlyCollection<SomeImmutableType> PropName
{
    get
    {
        if(null == m_PropName)
        {
            ReadOnlyCollection<SomeImmutableType> temp = /* do lazy init */;
            System.Threading.Interlocked(ref m_PropName, temp, null);
        }
        return m_PropName;
    }
}

Dies soll sicherstellen, dass alle Threads, die diese Methode auf dieser Objektinstanz rufen Sie einen Verweis auf das gleiche Objekt erhalten wird, so wird der Operator == arbeiten. Es ist möglich, verschwendete Arbeit zu haben, was in Ordnung ist -. Es macht dies einen optimistischer Algorithmus

Wie weiter unten in einigen Kommentaren erwähnt, hängt dies von dem Modell .NET 2.0-Speichers zu arbeiten. Andernfalls sollte m_PropName volatile deklariert werden.

War es hilfreich?

Lösung

Das wird funktionieren. Das Schreiben auf Referenzen in C # ist garantiert atomar sein, wie in Abschnitt 5.5 des spec . Dies ist wahrscheinlich immer noch nicht ein guter Weg, es zu tun, weil Ihr Code mehr verwirrend zu debuggen sein wird und im Gegenzug für eine vermutlich geringe Auswirkungen auf die Leistung lesen.

Jon Skeet hat eine große rel="nofollow.

Die allgemeine Beratung über kleine Optimierungen wie diese ist sie nicht zu tun, wenn ein Profiler Sie Dieser Code teilt ein Hotspot ist. Außerdem sollten Sie vorsichtig sein, Code zu schreiben, die nicht vollständig von den meisten Programmierer verstanden werden kann, ohne die Spezifikation zu überprüfen.

EDIT: Wie in den Kommentaren erwähnt, auch wenn Sie sagen, Sie nicht, wenn zwei Versionen des Objekts nichts ausmachen erstellt bekommen, diese Situation ist so kontraintuitiv, dass dieser Ansatz sollte niemals verwendet werden.

Andere Tipps

Sie sollten eine Sperre verwenden. Andernfalls riskieren Sie zwei Instanzen von m_PropName bestehenden und im Gebrauch von verschiedenen Threads. Dies kann kein Problem in vielen Fällen sein; Wenn Sie jedoch == zu verwenden, anstatt .equals() der Lage sein wollen, dann wird dies ein Problem sein. Seltene Rennbedingungen sind nicht die besseren Fehler zu haben. Sie sind schwer zu debuggen und zu reproduzieren.

In Ihrem Code, wenn zwei verschiedene Threads gleichzeitig Ihre Immobilie PropName erhalten (zum Beispiel auf einem Multi-Core-CPU), dann können sie verschiedene neue Instanzen der Eigenschaft erhalten, die identischen Daten enthalten, aber nicht die gleiche Objektinstanz sein.

Ein wesentlicher Vorteil der unveränderlichen Objekte ist, dass == zu .equals() äquivalent ist, die Verwendung des leistungsfähigere == um Vergleiche zu ermöglichen. Wenn Sie nicht in der faulen Initialisierung synchronisieren, dann riskieren Sie diesen Vorteil zu verlieren.

Sie verlieren auch Unveränderlichkeit. Ihr Objekt wird mit verschiedenen Objekten initialisiert zweimal wird (die die gleichen Werte enthalten), so einen Thread, der bereits den Wert Ihrer Immobilie erhalten, aber das wird es wieder erhalten kann ein anderes Objekt zum zweiten Mal.

Ich würde mich interessieren andere Antworten auf diese zu hören, aber ich habe kein Problem mit ihm sehen. Die doppelte Kopie aufgegeben werden und wird GCed.

Sie müssen allerdings das Feld volatile machen.

In Bezug auf diese:

  

Aber ich bin überrascht, niemand brachte   up die Möglichkeit des Compilers   zu realisieren, dass die Variable TEMP ist,   nicht notwendig und nur die Zuordnung   gerade zu m_PropName. Wäre das   der Fall ist, dann könnte ein Lesefaden   vielleicht lesen ein Objekt, das nicht hat   fertig gebaut. Ist die   Compiler verhindern eine solche Situation?

Ich hielt es zu erwähnen, aber es macht keinen Unterschied. Der neue Betreiber keine Referenz zurückgeben (und so die Zuordnung zum Feld nicht passiert), bis der Konstruktor abgeschlossen -. Das von der Laufzeit garantiert ist, nicht der Compiler

Allerdings ist die Sprache / Laufzeit garantiert nicht wirklich, dass andere Threads nicht ein teilweise konstruiertes Objekt sehen können - es hängt davon ab, was der Konstruktor ist.

Update:

Die OP fragt sich auch, ob diese Seite hat eine hilfreiche Idee . Ihr endgültiger Code-Snippet ist eine Instanz von doppelt geprüft Verriegelung , das das klassische Beispiel für eine Idee, die tausende von Menschen zueinander recommmend ohne eine Vorstellung davon, wie es richtig zu machen. Das Problem ist, dass SMP-Maschinen von mehreren CPUs mit ihren eigenen Speicher-Caches bestehen. Wenn sie haben ihre Caches zu synchronisieren jedes Mal eine Speicher-Update war, würde dies die Vorteile des mit mehreren CPUs rückgängig machen. So sie synchronisieren nur bei einer „Memory-Barriere“, die auftritt, wenn eine Sperre wird herausgenommen, oder eine verriegelte Operation auftritt, oder eine volatile Variable zugegriffen wird.

Die übliche Reihenfolge der Ereignisse ist:

  • Coder entdeckt doppelt geprüft Verriegelung
  • Coder entdeckt Speicher Barrieren

Zwischen diesen beiden Ereignissen, geben sie eine Menge zerbrochener Software.

Auch glauben viele Menschen (wie der Typ der Fall ist), die Sie „beseitigen Sperren“ kann durch die Verwendung verzahnt Operationen. Aber während der Laufzeit sind sie eine Speicherbarriere und so führen sie alle CPUs ihren Caches zu beenden und synchronisieren. Sie haben einen Vorteil gegenüber Schleusen, dass sie nicht brauchen, einen Anruf in das OS-Kernel zu machen (sie sind „Benutzercode“ nur), aber sie können die Leistung töten genauso viel wie jeder Synchronisationstechnik .

Zusammenfassung: Threading-Code sieht etwa 1000 x leichter zu schreiben, als es ist

.

Ich bin für faul init, wenn die Daten nicht immer zugegriffen werden kann, und es kann eine gute Menge an Ressourcen, um die Daten zu holen oder zu speichern.

Ich denke, es ein Schlüsselbegriff wird hier vergessen. Gemäß den C # Design-Konzepte, Sie sollten nicht Ihre Instanz Mitglieder Thread-sicher standardmäßig machen nur statische Mitglieder sollten Thread-sicher standardmäßig vorgenommen werden. Es sei denn, Sie einige statische / globale Daten zugreifen, sollten Sie nicht zusätzliche Schlösser in Ihren Code hinzufügen.

Von dem, was Ihr Code zeigt, ist der faule init alle innerhalb einer Instanz Eigenschaft, so würde ich nicht Schlösser, um es hinzuzufügen. Wenn durch Design ist gemeint, durch mehrere Threads gleichzeitig zugegriffen werden, dann gehen Sie vor und die Sperre hinzuzufügen.

By the way, kann es keinen Code von viel reduzieren, aber ich bin Fan des Null-coalesce Operators. Der Körper zu Ihrem Getter könnte dies stattdessen werden:
m_PropName = m_PropName ?? new ...();
return m_PropName;

Es wird von den zusätzlichen "if (m_PropName == null) ..." befreien und meiner Meinung nach macht es übersichtlicher und lesbarer.

Ich bin kein C # Experte, aber soweit ich das beurteilen kann, stellt dies nur ein Problem, wenn Sie, dass nur eine Instanz von Readonlycollection erfordern wird erstellt. Sie sagen, dass das erstellte Objekt wird immer das gleiche sein, und es spielt keine Rolle, wenn zwei (oder mehr) Threads eine neue Instanz zu tun schaffen, so würde ich sagen, dass es ok ist dies ohne eine Sperre zu tun.

Eine Sache, die ein seltsamer Fehler später wäre werden könnte, wenn man für die Gleichstellung der Instanzen vergleichen würde, was würde manchmal nicht gleich sein. Aber wenn Sie denken, dass im Kopf (oder einfach nicht tun) ich keine anderen Probleme zu sehen.

Leider müssen Sie eine Sperre. Es gibt eine Menge von sehr subtil Fehler, wenn Sie nicht richtig verriegeln. sucht ein gewaltiges Beispiel in dieser Antwort .

Ein sicher faul Initialisierung ohne Sperre verwenden, wenn das Feld nur geschrieben werden, wenn sie entweder leer oder bereits hält entweder der Wert geschrieben werden oder, in einigen Fällen ein Äquivalent . Beachten Sie, dass keine zwei veränderbare Objekte äquivalent sind; ein Feld, das mit einer Referenz geschrieben werden kann, einen Verweis auf ein veränderbares Objekt enthält nur das gleiche Objekt (den Schreib bedeutet keine Wirkung haben würde).

Es gibt drei allgemeine Muster ein für faule Initialisierung verwenden kann, abhängig von den Umständen:

  1. eine Sperre verwenden, wenn der Wert der Berechnung wäre teuer zu schreiben, und man wünscht, eine solche Anstrengung aufwenden unnötig zu vermeiden. Die doppelt überprüft Verriegelungsmuster ist gut auf Systeme, deren Speichermodell unterstützt.
  2. Wenn man einen unveränderlichen Wert zu speichern, es zu berechnen, wenn es scheint notwendig zu sein, und es nur speichern. Andere Threads, die eine redundante Berechnung nicht den Laden sehen kann durchführen, aber sie werden einfach versuchen, das Feld mit dem Wert zu schreiben, die bereits vorhanden ist.
  3. Wenn man einen Verweis auf eine billige zu produzieren wandelbar Klassenobjekt zu speichern, um ein neues Objekt erstellen, wenn es notwendig zu sein scheint, und verwenden Sie dann `Interlocked.CompareExchange` es zu speichern, wenn das Feld immer noch leer.

Beachten Sie, dass, wenn man in einem Thread auf jedem Zugriff auf andere als die erste Verriegelung, so dass der faule Leser Thread-sicher nicht verhängen eine erhebliche Leistungseinbußen vermeiden. Während es üblich ist für veränderbare Klassen nicht Thread-sicher zu sein, alle Klassen, das für jede Kombination von Lesern Aktionen zu sein unveränderlich Anspruch sollten 100% threadsicher sein. Jede Klasse, die nicht erfüllen kann, eine solche fadenSicherheitsAnforderung sollte nicht behaupten, unveränderlich zu sein.

Dies ist definitiv ein Problem.

Betrachten Sie dieses Szenario: Thread „A“ greift auf die Eigenschaft, und die Sammlung initialisiert wird. Bevor es die lokale Instanz in das Feld „m_PropName“ zuordnet, Thread „B“ greift auf die Eigenschaft, außer es zu vollenden wird. Thread „B“ hat nun eine Bezugnahme auf die Instanz, die derzeit in „m_PropName“ gespeichert ist, ..., bis Thread „A“ fort, wobei an diesem Punkt „m_PropName“ von der lokalen Instanz in diesem Thread überschrieben wird.

Es gibt jetzt ein paar Probleme. Als erstes Thema „B“ nicht die richtige Instanz mehr hat, da der Besitz Objekt denkt, dass „m_PropName“ die einzige Instanz ist, aber es sickerte eine initialisierte Instanz, wenn Thread „B“ abgeschlossen, bevor Thread „A“. Ein weiterer Grund ist, wenn die Sammlung zwischen wenn Thread „A“ geändert und Faden „B“ hat ihre Instanzen. Dann haben Sie eine falsche Daten. Es könnte noch schlimmer sein, wenn Sie beobachten oder die schreibgeschützte Sammlung Modifizieren intern (was natürlich können Sie nicht mit Readonlycollection, konnte aber, wenn Sie es mit einer anderen Implementierung ersetzt, die Sie über Ereignisse beobachten konnten oder modifizieren intern aber nicht extern).

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top