.NET - Wörterbuchsperre vs. ConcurrentDictionary


131

Ich konnte nicht genügend Informationen zu ConcurrentDictionaryTypen finden und dachte, ich würde hier danach fragen.

Derzeit verwende ich a Dictionary, um alle Benutzer zu speichern, auf die ständig von mehreren Threads zugegriffen wird (aus einem Thread-Pool, also keine genaue Anzahl von Threads), und der Zugriff ist synchronisiert.

Ich habe kürzlich herausgefunden, dass es in .NET 4.0 eine Reihe von thread-sicheren Sammlungen gibt, und es scheint sehr erfreulich zu sein. Ich habe mich gefragt, was die Option "effizienter und einfacher zu verwalten" wäre, da ich die Option habe, einen normalen Dictionarymit synchronisiertem Zugriff zu haben oder einen, ConcurrentDictionaryder bereits threadsicher ist.

Verweis auf .NET 4.0 ConcurrentDictionary


Antworten:


146

Eine thread-sichere Sammlung im Vergleich zu einer nicht thread-sicheren Sammlung kann auf andere Weise betrachtet werden.

Stellen Sie sich ein Geschäft ohne Angestellten vor, außer an der Kasse. Sie haben eine Menge Probleme, wenn die Leute nicht verantwortungsbewusst handeln. Nehmen wir zum Beispiel an, ein Kunde nimmt eine Dose aus einer Pyramidendose, während ein Angestellter gerade die Pyramide baut. Die Hölle bricht los. Oder was ist, wenn zwei Kunden gleichzeitig nach demselben Artikel greifen und wer gewinnt? Wird es einen Kampf geben? Dies ist eine nicht threadsichere Sammlung. Es gibt viele Möglichkeiten, um Probleme zu vermeiden, aber alle erfordern eine Art Sperre oder eher einen expliziten Zugriff auf die eine oder andere Weise.

Stellen Sie sich andererseits ein Geschäft mit einem Angestellten an einem Schreibtisch vor, und Sie können nur über ihn einkaufen. Du stellst dich an und fragst ihn nach einem Gegenstand, er bringt ihn dir zurück und du verlässt die Schlange. Wenn Sie mehrere Artikel benötigen, können Sie auf jeder Rundreise nur so viele Artikel abholen, wie Sie sich erinnern können. Sie müssen jedoch vorsichtig sein, um den Angestellten nicht zu belästigen. Dies wird die anderen Kunden in der Schlange hinter Ihnen verärgern.

Betrachten Sie dies nun. Was ist, wenn Sie im Laden mit einem Angestellten ganz vorne mit dabei sind und den Angestellten fragen: "Haben Sie Toilettenpapier?", Und er sagt "Ja", und dann sagen Sie "Ok, ich". Ich melde mich bei Ihnen, wenn ich weiß, wie viel ich brauche. Wenn Sie dann wieder ganz vorne mit dabei sind, kann der Laden natürlich ausverkauft sein. Dieses Szenario wird durch eine threadsichere Sammlung nicht verhindert.

Eine threadsichere Sammlung garantiert, dass ihre internen Datenstrukturen jederzeit gültig sind, auch wenn von mehreren Threads aus darauf zugegriffen wird.

Für eine nicht threadsichere Sammlung gibt es keine solchen Garantien. Wenn Sie beispielsweise einem Binärbaum in einem Thread etwas hinzufügen, während ein anderer Thread damit beschäftigt ist, den Baum neu auszugleichen, gibt es keine Garantie dafür, dass das Element hinzugefügt wird, oder auch wenn der Baum danach noch gültig ist, kann er hoffnungslos beschädigt sein.

Eine threadsichere Sammlung garantiert jedoch nicht, dass sequentielle Operationen am Thread alle mit demselben "Snapshot" seiner internen Datenstruktur arbeiten. Wenn Sie also Code wie diesen haben:

if (tree.Count > 0)
    Debug.WriteLine(tree.First().ToString());

Möglicherweise erhalten Sie eine NullReferenceException, da zwischen tree.Countund ein tree.First()anderer Thread die verbleibenden Knoten im Baum gelöscht hat, was bedeutet, dass zurückgegeben First()wird null.

In diesem Szenario müssen Sie entweder prüfen, ob die betreffende Sammlung einen sicheren Weg bietet, um das zu erhalten, was Sie möchten. Möglicherweise müssen Sie den obigen Code neu schreiben oder Sie müssen möglicherweise sperren.


9
Jedes Mal, wenn ich diesen Artikel lese, obwohl alles wahr ist, gehen wir irgendwie den falschen Weg.
scope_creep

19
Das Hauptproblem in einer Thread-fähigen Anwendung sind veränderbare Datenstrukturen. Wenn Sie das vermeiden können, sparen Sie sich jede Menge Ärger.
Lasse V. Karlsen

89
Dies ist eine nette Erklärung für die Thread-Sicherheit, aber ich kann nicht anders, als das Gefühl zu haben, dass dies die Frage des OP nicht wirklich angesprochen hat. Was das OP fragte (und worauf ich später bei der Suche nach dieser Frage stieß), war der Unterschied zwischen der Verwendung eines Standards Dictionaryund der Handhabung der Sperre selbst und der Verwendung des ConcurrentDictionaryin .NET 4+ integrierten Typs. Ich bin eigentlich ein bisschen verblüfft, dass dies akzeptiert wurde.
mclark1129

4
Thread-Sicherheit ist mehr als nur die Verwendung der richtigen Kollektion. Die Verwendung der richtigen Sammlung ist ein Anfang, nicht das einzige, mit dem Sie sich befassen müssen. Ich denke, das wollte das OP wissen. Ich kann nicht erraten, warum dies akzeptiert wurde, es ist eine Weile her, seit ich das geschrieben habe.
Lasse V. Karlsen

2
Ich denke, was @ LasseV.Karlsen zu sagen versucht, ist, dass ... Thread-Sicherheit leicht zu erreichen ist, aber Sie müssen ein gewisses Maß an Parallelität in Bezug auf den Umgang mit dieser Sicherheit angeben. Fragen Sie sich Folgendes: "Kann ich mehr Vorgänge gleichzeitig ausführen, während das Objekt threadsicher ist?" Betrachten Sie dieses Beispiel: Zwei Kunden möchten unterschiedliche Artikel zurückgeben. Wenn Sie als Manager die Reise unternehmen, um Ihr Geschäft wieder aufzufüllen, können Sie dann nicht einfach beide Artikel auf einer Reise auffüllen und trotzdem die Anforderungen beider Kunden bearbeiten, oder werden Sie jedes Mal eine Reise unternehmen, wenn ein Kunde einen Artikel zurückgibt?
Alexandru

68

Sie müssen immer noch sehr vorsichtig sein, wenn Sie threadsichere Sammlungen verwenden, da threadsicher nicht bedeutet, dass Sie alle Threading-Probleme ignorieren können. Wenn sich eine Sammlung als threadsicher bewirbt, bedeutet dies normalerweise, dass sie auch dann in einem konsistenten Zustand bleibt, wenn mehrere Threads gleichzeitig lesen und schreiben. Dies bedeutet jedoch nicht, dass ein einzelner Thread eine "logische" Folge von Ergebnissen sieht, wenn er mehrere Methoden aufruft.

Wenn Sie beispielsweise zuerst prüfen, ob ein Schlüssel vorhanden ist, und später den Wert abrufen, der dem Schlüssel entspricht, ist dieser Schlüssel möglicherweise auch bei einer ConcurrentDictionary-Version nicht mehr vorhanden (da ein anderer Thread den Schlüssel möglicherweise entfernt hat). In diesem Fall müssen Sie weiterhin die Sperrung verwenden (oder besser: Kombinieren Sie die beiden Aufrufe mithilfe von TryGetValue ).

Verwenden Sie sie also, aber denken Sie nicht, dass Sie damit kostenlos alle Parallelitätsprobleme ignorieren können. Sie müssen immer noch vorsichtig sein.


4
Sie sagen also im Grunde, wenn Sie das Objekt nicht richtig verwenden, wird es nicht funktionieren?
ChaosPandion

3
Grundsätzlich müssen Sie TryAdd anstelle der üblichen Contains -> Add verwenden.
ChaosPandion

3
@Bruno: Nein. Wenn Sie nicht gesperrt haben, hat möglicherweise ein anderer Thread den Schlüssel zwischen den beiden Aufrufen entfernt.
Mark Byers

13
Ich will hier nicht knapp klingen, war aber TryGetValueimmer ein Teil des Dictionaryempfohlenen Ansatzes. Die wichtigen "gleichzeitigen" Methoden, die ConcurrentDictionaryeingeführt werden , sind AddOrUpdateund GetOrAdd. Also, gute Antwort, hätte aber ein besseres Beispiel wählen können.
Aaronaught

4
Richtig - im Gegensatz zu Datenbanken mit Transaktionen, bei denen die meisten gleichzeitigen Suchstrukturen im Speicher das Konzept der Isolation verfehlen , garantieren sie nur, dass die interne Datenstruktur korrekt ist.
Chang

39

Intern verwendet ConcurrentDictionary für jeden Hash-Bucket eine separate Sperre. Solange Sie nur Add / TryGetValue und ähnliche Methoden verwenden, die für einzelne Einträge funktionieren, funktioniert das Wörterbuch als nahezu sperrenfreie Datenstruktur mit dem jeweiligen süßen Leistungsvorteil. OTOH Die Aufzählungsmethoden (einschließlich der Count-Eigenschaft) sperren alle Buckets auf einmal und sind daher in Bezug auf die Leistung schlechter als ein synchronisiertes Wörterbuch.

Ich würde sagen, verwenden Sie einfach ConcurrentDictionary.


15

Ich denke, dass die ConcurrentDictionary.GetOrAdd-Methode genau das ist, was die meisten Multithread-Szenarien benötigen.


1
Ich würde auch TryGet verwenden, andernfalls verschwenden Sie den Aufwand, den zweiten Parameter jedes Mal auf GetOrAdd zu initialisieren, wenn der Schlüssel bereits vorhanden ist.
Jorrit Schippers

7
GetOrAdd hat die Überladung GetOrAdd (TKey, Func <TKey, TValue>) , sodass der zweite Parameter nur dann verzögert initialisiert werden kann, wenn der Schlüssel nicht im Wörterbuch vorhanden ist. Außerdem wird TryGetValue zum Abrufen von Daten verwendet, nicht zum Ändern. Nachfolgende Aufrufe jeder dieser Methoden können dazu führen, dass ein anderer Thread den Schlüssel zwischen den beiden Aufrufen hinzugefügt oder entfernt hat.
sgnsajgon

Dies sollte die markierte Antwort auf die Frage sein, obwohl Massees Beitrag ausgezeichnet ist.
Spivonious

13

Haben Sie die Reactive Extensions für .Net 3.5sp1 gesehen ? Laut Jon Skeet haben sie ein Bündel der parallelen Erweiterungen und gleichzeitigen Datenstrukturen für .Net3.5 sp1 zurückportiert.

Für .Net 4 Beta 2 gibt es eine Reihe von Beispielen, in denen die Verwendung der parallelen Erweiterungen ausführlich beschrieben wird.

Ich habe gerade die letzte Woche damit verbracht, das ConcurrentDictionary mit 32 Threads zu testen, um E / A durchzuführen. Es scheint wie angekündigt zu funktionieren, was darauf hindeutet, dass eine enorme Menge an Tests durchgeführt wurde.

Bearbeiten : .NET 4 ConcurrentDictionary und Muster.

Microsoft hat ein PDF mit dem Namen Patterns of Paralell Programming veröffentlicht. Es lohnt sich wirklich, es herunterzuladen, da es die richtigen Muster für .Net 4 Concurrent-Erweiterungen und die zu vermeidenden Anti-Muster ausführlich beschreibt. Hier ist es.


4

Grundsätzlich möchten Sie mit dem neuen ConcurrentDictionary gehen. Sie müssen sofort weniger Code schreiben, um threadsichere Programme zu erstellen.


Gibt es ein Problem, wenn ich mit Concurrent Dictionery fortfahre?
kbvishnu

1

Dies ConcurrentDictionaryist eine großartige Option, wenn alle Ihre Sicherheitsanforderungen erfüllt sind . Wenn dies nicht der Fall ist , mit anderen Worten, wenn Sie etwas etwas Komplexes tun, ist ein normales Dictionary+ lockmöglicherweise die bessere Option. Angenommen, Sie fügen einem Wörterbuch einige Bestellungen hinzu und möchten den Gesamtbetrag der Bestellungen auf dem neuesten Stand halten. Sie können Code wie folgt schreiben:

private ConcurrentDictionary<int, Order> _dict;
private Decimal _totalAmount = 0;

while (await enumerator.MoveNextAsync())
{
    Order order = enumerator.Current;
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

Dieser Code ist nicht threadsicher. Mehrere Threads, die das _totalAmountFeld aktualisieren , können es in einem beschädigten Zustand belassen. Sie können also versuchen, es mit einem lock:

_dict.TryAdd(order.Id, order);
lock (_locker) _totalAmount += order.Amount;

Dieser Code ist "sicherer", aber immer noch nicht threadsicher. Es gibt keine Garantie dafür, dass das _totalAmountmit den Einträgen im Wörterbuch übereinstimmt. Ein anderer Thread versucht möglicherweise, diese Werte zu lesen, um ein UI-Element zu aktualisieren:

Decimal totalAmount;
lock (_locker) totalAmount = _totalAmount;
UpdateUI(_dict.Count, totalAmount);

Die totalAmountAnzahl entspricht möglicherweise nicht der Anzahl der Bestellungen im Wörterbuch. Die angezeigten Statistiken könnten falsch sein. An diesem Punkt werden Sie feststellen, dass Sie den lockSchutz um die Aktualisierung des Wörterbuchs erweitern müssen:

// Thread A
lock (_locker)
{
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

// Thread B
int ordersCount;
Decimal totalAmount;
lock (_locker)
{
    ordersCount = _dict.Count;
    totalAmount = _totalAmount;
}
UpdateUI(ordersCount, totalAmount);

Dieser Code ist absolut sicher, aber alle Vorteile der Verwendung von a ConcurrentDictionarysind weg.

  1. Die Leistung ist schlechter geworden als bei Verwendung eines normalen Geräts Dictionary, da die interne Verriegelung im Inneren ConcurrentDictionaryjetzt verschwenderisch und überflüssig ist.
  2. Sie müssen Ihren gesamten Code auf ungeschützte Verwendung der gemeinsam genutzten Variablen überprüfen.
  3. Sie müssen keine umständliche API verwenden ( TryAdd?, AddOrUpdate?).

Mein Rat lautet also: Beginnen Sie mit einem Dictionary+ lockund behalten Sie die Option, später ein Upgrade auf a ConcurrentDictionaryals Leistungsoptimierung durchzuführen, wenn diese Option tatsächlich praktikabel ist. In vielen Fällen wird es nicht sein.


0

Wir haben ConcurrentDictionary für die zwischengespeicherte Sammlung verwendet, die alle 1 Stunde neu gefüllt und dann von mehreren Client-Threads gelesen wird, ähnlich der Lösung für Ist dieser Beispiel-Thread sicher? Frage.

Wir haben festgestellt, dass die Änderung in  ReadOnlyDictionary  die Gesamtleistung verbessert.


ReadOnlyDictionary ist nur der Wrapper um ein anderes IDictionary. Was benutzt du unter der Haube?
Lu55

@ Lu55, wir haben die MS .Net-Bibliothek verwendet und zwischen verfügbaren / anwendbaren Wörterbüchern ausgewählt. Mir ist die interne Implementierung von ReadOnlyDictionary
Michael Freidgeim
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.