Ist es in Ordnung, vor und in einer Sperre zu prüfen, bevor der Code ausgeführt wird?

8

Beim Arbeiten mit Thread-Sicherheit, finde ich mich immer "Doppelprüfung" vor dem Ausführen von Code in einem Sperrblock und ich fragte mich, ob ich das Richtige tat. Betrachten Sie die folgenden drei Möglichkeiten, dasselbe zu tun:

Beispiel 1:

%Vor%

Beispiel 2:

%Vor%

Beispiel 3:

%Vor%

Ich lehne mich immer zu Beispiel 3 hin, und deshalb denke ich, dass ich das Richtige tue

  • Thread 1 gibt DoSomething(string) ein
  • MyCollection[key] == null also Thread 1 erhält eine Sperre, genau wie Thread 2
  • eingibt
  • MyCollection[key] == null ist immer noch wahr, daher wartet Thread 2 auf die Sperre
  • Thread 1 berechnet den Wert für MyCollection[key] und fügt ihn zur Sammlung
  • hinzu
  • Thread 1 gibt die Sperre frei und ruft DoSomethingWithResult(MyCollection[key]); auf
  • Thread 2 erhält die Sperre, zu welcher Zeit MyCollection[key] != null
  • Thread 2 tut nichts, gibt die Sperre frei und geht weiter auf seinen fröhlichen Weg

Beispiel 1 würde funktionieren, aber es besteht ein großes Risiko, dass Thread 2 MyCollection[key] redundant berechnen könnte.

Beispiel 2 würde funktionieren, aber jeder Thread würde eine Sperre erhalten, selbst wenn es nicht nötig wäre - was ein (zugegebenermaßen sehr kleiner) Flaschenhals sein könnte. Warum halten Sie Threads, wenn Sie nicht brauchen?

Überdenke ich das und wenn ja, was ist der bevorzugte Weg, um mit diesen Situationen umzugehen?

    
Iain Fraser 06.11.2012, 23:55
quelle

4 Antworten

5

Die erste Methode sollte nicht verwendet werden. Wie Sie festgestellt haben, ist es undicht, so dass mehr als ein Thread am Ende der teuren Methode enden kann. Je länger diese Methode dauert, desto größer ist das Risiko, dass ein anderer Thread sie auch ausführt. In den meisten Fällen ist es nur ein Leistungsproblem, aber in einigen Fällen kann es auch ein Problem sein, dass die resultierenden Daten später durch einen neuen Datensatz ersetzt werden.

Die zweite Methode ist die gebräuchlichste Methode. Die dritte Methode wird verwendet, wenn auf die Daten so häufig zugegriffen wird, dass das Sperren zu einem Leistungsproblem wird.

    
Guffa 07.11.2012, 00:17
quelle
3

Ich schlage vor, dass Sie dieses Problem den Profis überlassen und das ConcurrentDictionary verwenden (ich weiß, dass ich würde). Es hat die Methode GetOrAdd , die genau das tut, was Sie wollen und die garantiert funktioniert.

    
fsimonazzi 07.11.2012 00:33
quelle
3

Ich werde eine gewisse Unsicherheit einführen, weil das Problem nicht trivial ist. Grundsätzlich stimme ich Guffa zu und würde mich für ein zweites Beispiel entscheiden. Es ist, weil der erste ist gebrochen, während der dritte wiederum, trotz der Tatsache, scheint optimiert zu sein, ist schwierig. Deshalb konzentriere ich mich hier auf den dritten:

%Vor%

Auf den ersten Blick kann es als Verbesserung der Performance auftreten, ohne die ganze Zeit zu sperren, aber es ist auch problematisch, aufgrund des Speichermodells (Lesevorgänge können vor den Schreibvorgängen neu angeordnet werden) oder aggressive Compileroptimierung ( reference ), zum Beispiel :

  1. Thread A stellt fest, dass der Wert item nicht initialisiert ist. Daher erhält er die Sperre und beginnt damit, den Wert zu initialisieren.
  2. Aufgrund von Speichermodellen, Compileroptimierungen usw. darf der vom Compiler generierte Code die gemeinsam genutzte Variable so aktualisieren, dass sie auf ein teilweise konstruiertes Objekt zeigt, bevor A die Initialisierung beendet hat.
  3. Thread B stellt fest, dass die freigegebene Variable initialisiert wurde (oder so aussieht) und gibt ihren Wert zurück. Da der Thread B glaubt, dass der Wert bereits initialisiert wurde, erhält er die Sperre nicht. Wenn die Variable verwendet wird, bevor A die Initialisierung beendet, wird das Programm wahrscheinlich abstürzen.

Es gibt Lösungen für dieses Problem:

  1. Sie können item als flüchtige Variable definieren, die sicherstellt, dass die Variable immer aktuell ist. Volatile wird verwendet, um eine Speicherbarriere zwischen Lese- und Schreibvorgängen für die Variable zu erstellen.

    (siehe Die Notwendigkeit eines flüchtigen Modifikators in Double checked Sperren in .NET und Implementieren des Singleton-Musters in C # )

  2. Sie können MemoryBarrier ( item nichtflüchtig) verwenden:

    %Vor%

    Der Prozessor, der den aktuellen Thread ausführt, kann Anweisungen nicht so umordnen, dass Speicherzugriffe vor dem Aufruf von MemoryBarrier nach Speicherzugriffen ausgeführt werden, die auf den Aufruf von MemoryBarrier folgen.

    (siehe Thread.MemoryBarrier-Methode und Thema )

UPDATE: Double-Check Locking funktioniert, wenn es korrekt implementiert wird, in C # einwandfrei. Für weitere Details überprüfen Sie zusätzliche Referenzen, z. MSDN , MSDN Magazin und diese Antwort .

    
jwaliszko 07.11.2012 01:35
quelle
1

Es gibt eine Vielzahl von Mustern, die man für die Erstellung von Lazy Objects verwenden kann, auf die sich Ihre Code-Beispiele zu konzentrieren scheinen. Eine andere Variante, die manchmal nützlich sein kann, wenn Ihre Sammlung etwas wie ein Array oder ConcurrentDictionary ist, das es dem Code ermöglicht, automatisch zu prüfen, ob ein Wert bereits gesetzt wurde, und dies nur zu schreiben, wäre:

%Vor%

Dieser Code geht davon aus, dass es möglich ist, eine Dummy-Instanz eines Typs, der von Thing abgeleitet ist, kostengünstig zu erstellen. Das neue Objekt sollte nicht ein Singleton sein oder anderweitig wiederverwendet werden. Jeder Slot in myArray wird zweimal geschrieben - zuerst mit einem vordefinierten Dummy-Objekt und dann mit dem realen Objekt. Nur ein Thread kann ein Dummy-Objekt schreiben, und nur der Thread, der erfolgreich ein Dummy-Objekt geschrieben hat, kann das echte Objekt schreiben. Jeder andere Thread sieht entweder ein reales Objekt (in diesem Fall ist das Objekt vollständig initialisiert) oder ein Dummy-Objekt, das gesperrt wird, bis das Array mit einem Verweis auf das reelle Objekt aktualisiert wurde.

Im Gegensatz zu den anderen oben gezeigten Ansätzen ermöglicht dieser Ansatz die gleichzeitige Initialisierung verschiedener Elemente im Array. Der einzige Zeitpunkt, an dem die Dinge blockiert werden, ist der Versuch, auf ein Objekt zuzugreifen, dessen Initialisierung läuft.

    
supercat 22.08.2013 20:12
quelle