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
DoSomething(string)
ein
MyCollection[key] == null
also Thread 1 erhält eine Sperre, genau wie Thread 2 MyCollection[key] == null
ist immer noch wahr, daher wartet Thread 2 auf die Sperre MyCollection[key]
und fügt ihn zur Sammlung DoSomethingWithResult(MyCollection[key]);
auf
MyCollection[key] != null
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?
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.
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.
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 :
item
nicht initialisiert ist. Daher erhält er die Sperre und beginnt damit, den Wert zu initialisieren. Es gibt Lösungen für dieses Problem:
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 # )
Sie können MemoryBarrier
( item
nichtflüchtig) verwenden:
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 .
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:
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.
Tags und Links c# multithreading thread-safety