Ich war begeistert, den neuen System.Collections.Concurrent
Namespace in .Net 4.0 zu sehen, ganz nett! Ich habe gesehen ConcurrentDictionary
, ConcurrentQueue
, ConcurrentStack
, ConcurrentBag
und BlockingCollection
.
Eine Sache, die auf mysteriöse Weise zu fehlen scheint, ist a ConcurrentList<T>
. Muss ich das selbst schreiben (oder aus dem Internet holen :))?
Vermisse ich hier etwas Offensichtliches?
Antworten:
Ich habe es vor einiger Zeit versucht (auch: auf GitHub ). Meine Implementierung hatte einige Probleme, auf die ich hier nicht eingehen werde. Lassen Sie mich vor allem sagen, was ich gelernt habe.
Erstens gibt es keine Möglichkeit, eine vollständige Implementierung zu erhalten
IList<T>
, die sperrenlos und threadsicher ist. Insbesondere werden zufällige Einfügungen und Entfernungen nicht funktionieren, es sei denn, Sie vergessen auch den O (1) -Wahlzugriff (dh, Sie "betrügen" und verwenden einfach eine verknüpfte Liste und lassen die Indizierung saugen).Was ich dachte , vielleicht war ein Thread-sicher, begrenzte Teilmenge von mir lohnen
IList<T>
: insbesondere eine , die eine erlauben würde ,Add
und bietet zufälligen Nur - Lese- Zugriff Index (aber nichtInsert
,RemoveAt
usw. und auch keine zufälligen Schreibzugriff).Dies war das Ziel meiner
ConcurrentList<T>
Implementierung . Als ich jedoch die Leistung in Multithread-Szenarien testete, stellte ich fest, dass das einfache Synchronisieren von Adds zu aList<T>
schneller war . Grundsätzlich ist das Hinzufügen zu aList<T>
bereits blitzschnell; Die Komplexität der beteiligten Rechenschritte ist winzig (Index erhöhen und einem Element in einem Array zuweisen; das war's wirklich ). Sie würden eine Menge gleichzeitiger Schreibvorgänge benötigen , um jegliche Art von Sperrenkonflikten zu sehen. und selbst dann würde die durchschnittliche Leistung jedes Schreibvorgangs die teurere, wenn auch sperrenlose Implementierung in übertreffenConcurrentList<T>
.In dem relativ seltenen Fall, dass die Größe des internen Arrays der Liste selbst geändert werden muss, zahlen Sie geringe Kosten. Letztendlich kam ich zu dem Schluss, dass dies das einzige Nischenszenario ist, in dem ein Nur-Add-
ConcurrentList<T>
Collection-Typ sinnvoll ist: Wenn Sie einen garantierten geringen Overhead für das Hinzufügen eines Elements bei jedem einzelnen Aufruf wünschen (im Gegensatz zu einem amortisierten Leistungsziel).Es ist einfach nicht annähernd so nützlich wie man denkt.
quelle
List<T>
das eine altmodische, monitorbasierte Synchronisation verwendet, ist esSynchronizedCollection<T>
in der BCL versteckt: msdn.microsoft.com/en-us/library/ms668265.aspxConcurrentList
Gewinn wäre, wäre, wenn nicht viel Aktivität zur Liste hinzugefügt wird, aber es gibt viele gleichzeitige Leser. Man könnte den Overhead der Leser auf eine einzige Speicherbarriere reduzieren (und selbst das beseitigen, wenn die Leser sich keine Gedanken über leicht veraltete Daten machen).ConcurrentList<T>
so zu konstruieren, dass die Leser garantiert einen konsistenten Zustand sehen, ohne dass eine Sperre erforderlich ist, mit relativ geringem zusätzlichen Aufwand. Wenn die Liste von z. B. Größe 32 auf 64 erweitert wird, behalten Sie das Array Größe 32 bei und erstellen Sie ein neues Array Größe 64. Wenn Sie jedes der nächsten 32 Elemente hinzufügen, setzen Sie es in Steckplatz 32-63 des neuen Arrays und kopieren Sie ein altes Element aus dem Array der Größe 32 in das neue. Bis das 64. Element hinzugefügt wird, suchen die Leser im Array der Größe 32 nach den Elementen 0-31 und im Array der Größe 64 nach den Elementen 32-63.Wofür würden Sie eine ConcurrentList verwenden?
Das Konzept eines Direktzugriffscontainers in einer Thread-Welt ist nicht so nützlich, wie es scheint. Die Aussage
insgesamt wäre immer noch nicht threadsicher.
Versuchen Sie, anstatt eine ConcurrentList zu erstellen, Lösungen mit den vorhandenen Komponenten zu erstellen. Die gebräuchlichsten Klassen sind die ConcurrentBag und insbesondere die BlockingCollection.
quelle
Monitor
dies ohnehin bereits verwenden können, gibt es keinen Grund für eine gleichzeitige Liste.Bei allem Respekt vor den bereits gegebenen großartigen Antworten möchte ich manchmal einfach einen thread-sicheren IList. Nichts fortgeschrittenes oder ausgefallenes. Leistung ist in vielen Fällen wichtig, aber manchmal ist das einfach kein Problem. Ja, es wird immer Herausforderungen ohne Methoden wie "TryGetValue" usw. geben, aber in den meisten Fällen möchte ich nur etwas, das ich aufzählen kann, ohne mir Sorgen machen zu müssen, dass alles gesperrt wird. Und ja, jemand kann wahrscheinlich einen "Fehler" in meiner Implementierung finden, der zu einem Deadlock oder etwas anderem führen könnte (nehme ich an), aber seien wir ehrlich: Wenn Sie beim Multithreading Ihren Code nicht richtig schreiben, ist dies der Fall geht sowieso ins Stocken. Vor diesem Hintergrund habe ich mich für eine einfache ConcurrentList-Implementierung entschieden, die diese Grundanforderungen erfüllt.
Und was es wert ist: Ich habe einen grundlegenden Test durchgeführt, bei dem 10.000.000 Elemente zur regulären Liste und ConcurrentList hinzugefügt wurden. Die Ergebnisse waren:
Liste beendet in: 7793 Millisekunden. Gleichzeitig beendet in: 8064 Millisekunden.
quelle
RemoveAt(int index)
ist nie threadsicher,Insert(int index, T item)
ist nur sicher für index == 0, die Rückgabe vonIndexOf()
ist sofort veraltet usw. Beginnen Sie nicht einmal mit demthis[int]
.ReaderWriterLockSlim
kann bei gleichzeitiger Verwendung leicht zum Deadlock gebracht werdenEnterUpgradeableReadLock()
. Sie verwenden es jedoch nicht, Sie machen die Sperre nicht nach außen zugänglich, und Sie rufen beispielsweise keine Methode auf, die eine Schreibsperre eingibt, während Sie eine Lesesperre halten, sodass die Verwendung Ihrer Klasse keine Deadlocks mehr verursacht wahrscheinlich.var l = new ConcurrentList<string>(); /* ... */ l[0] += "asdf";
. Im Allgemeinen kann jede Lese- / Schreibkombination bei gleichzeitiger Ausführung zu großen Problemen führen. Deshalb ist im Allgemeinen der gleichzeitigen Datenstrukturen Methoden für diejenigen bieten, wieConcurrentDictionary
‚sAddOrGet
usw. NB Ihre Konstante (und überflüssig , weil die Mitglieder sind bereits als solche durch einen Strich gekennzeichnet) Wiederholung vonthis.
clutters.ConcurrentList
(als anpassbares Array, nicht als verknüpfte Liste) ist mit nicht blockierenden Vorgängen nicht einfach zu schreiben. Die API lässt sich nicht gut in eine "gleichzeitige" Version übersetzen.quelle
Der Grund, warum es keine ConcurrentList gibt, ist, dass sie grundsätzlich nicht geschrieben werden kann. Der Grund dafür ist, dass einige wichtige Operationen in IList auf Indizes beruhen und dass dies einfach nicht funktioniert. Beispielsweise:
Der Effekt, den der Autor anstrebt, besteht darin, "Hund" vor "Katze" einzufügen. In einer Multithread-Umgebung kann jedoch alles mit der Liste zwischen diesen beiden Codezeilen geschehen. Zum Beispiel könnte ein anderer Thread
list.RemoveAt(0)
die gesamte Liste nach links verschieben, aber entscheidend ist, dass sich catIndex nicht ändert. Die Auswirkung hier ist, dass dieInsert
Operation tatsächlich den "Hund" hinter die Katze stellt, nicht davor.Die verschiedenen Implementierungen, die Sie als "Antworten" auf diese Frage sehen, sind gut gemeint, aber wie oben gezeigt, bieten sie keine zuverlässigen Ergebnisse. Wenn Sie wirklich eine listenähnliche Semantik in einer Multithread-Umgebung wünschen, können Sie nicht dorthin gelangen, indem Sie Sperren in die Listenimplementierungsmethoden einfügen. Sie müssen sicherstellen, dass jeder Index, den Sie verwenden, vollständig im Kontext der Sperre lebt. Das Ergebnis ist, dass Sie eine Liste in einer Multithread-Umgebung mit der richtigen Sperre verwenden können, aber die Liste selbst kann nicht in dieser Welt existieren.
Wenn Sie glauben, dass Sie eine gleichzeitige Liste benötigen, gibt es wirklich nur zwei Möglichkeiten:
Wenn Sie eine ConcurrentBag haben und sich in einer Position befinden, in der Sie sie als IList übergeben müssen, haben Sie ein Problem, da die von Ihnen aufgerufene Methode angegeben hat, dass sie möglicherweise versuchen, etwas wie oben mit der Katze & zu tun Hund. In den meisten Welten bedeutet dies, dass die von Ihnen aufgerufene Methode einfach nicht für die Verwendung in einer Multithread-Umgebung ausgelegt ist. Das bedeutet, dass Sie es entweder so umgestalten, dass es so ist, oder wenn Sie nicht können, müssen Sie sehr vorsichtig damit umgehen. Mit ziemlicher Sicherheit müssen Sie Ihre eigene Sammlung mit eigenen Sperren erstellen und die fehlerhafte Methode innerhalb einer Sperre aufrufen.
quelle
In Fällen, in denen Lesevorgänge die Anzahl der Schreibvorgänge erheblich übersteigen oder (wie häufig auch immer) Schreibvorgänge nicht gleichzeitig ausgeführt werden , kann ein Copy-on-Write- Ansatz angebracht sein.
Die unten gezeigte Implementierung ist
var snap = _list; snap[snap.Count - 1];
wird niemals ( naja , abgesehen von einer leeren Liste natürlich) geworfen, und Sie erhalten außerdem kostenlos eine thread-sichere Aufzählung mit Schnappschuss-Semantik. Wie ich Unveränderlichkeit LIEBE!Damit Copy-on-Write funktioniert, müssen Sie Ihre Datenstrukturen effektiv unveränderlich halten , dh niemand darf sie ändern, nachdem Sie sie anderen Threads zur Verfügung gestellt haben. Wenn Sie ändern möchten, Sie
Code
Verwendung
Wenn Sie mehr Leistung benötigen, wird es helfen , die Methode ungenerify, zum Beispiel ein Verfahren für jede Art von Modifikation erstellen (Hinzufügen, Entfernen, ...) Sie wollen, und harte Code , um die Funktionszeiger
cloner
undop
.NB # 1 Es liegt in Ihrer Verantwortung sicherzustellen, dass niemand die (angeblich) unveränderliche Datenstruktur ändert. In einer generischen Implementierung können wir nichts tun, um dies zu verhindern. Wenn
List<T>
Sie sich jedoch darauf spezialisieren, können Sie sich mit List.AsReadOnly () vor Änderungen schützen.NB # 2 Achten Sie auf die Werte in der Liste. Der obige Ansatz zum Kopieren beim Schreiben schützt nur die Listenmitgliedschaft. Wenn Sie jedoch keine Zeichenfolgen, sondern einige andere veränderbare Objekte einfügen möchten, müssen Sie sich um die Thread-Sicherheit kümmern (z. B. Sperren). Dies ist jedoch orthogonal zu dieser Lösung, und z. B. kann das Sperren der veränderlichen Werte problemlos ohne Probleme verwendet werden. Sie müssen sich nur dessen bewusst sein.
Hinweis Nr. 3 Wenn Ihre Datenstruktur sehr umfangreich ist und Sie sie häufig ändern, ist der Ansatz des Kopierens beim Schreiben möglicherweise sowohl hinsichtlich des Speicherverbrauchs als auch der CPU-Kosten für das Kopieren unerschwinglich. In diesem Fall möchten Sie möglicherweise stattdessen die unveränderlichen Sammlungen von MS verwenden.
quelle
System.Collections.Generic.List<t>
ist bereits threadsicher für mehrere Leser. Der Versuch, den Thread für mehrere Autoren sicher zu machen, wäre nicht sinnvoll. (Aus Gründen, die Henk und Stephen bereits erwähnt haben)quelle
Einige Leute haben einige Warenpunkte (und einige meiner Gedanken) hervorgehoben:
Das ist keine Antwort. Dies sind nur Kommentare, die nicht wirklich zu einem bestimmten Ort passen.
... Mein Fazit: Microsoft muss einige tiefgreifende Änderungen am "foreach" vornehmen, um die Verwendung der MultiThreaded-Sammlung zu vereinfachen. Außerdem muss es dort eigene Regeln für die Verwendung von IEnumerator befolgen. Bis dahin können wir leicht eine MultiThreadList schreiben, die einen blockierenden Iterator verwenden würde, aber nicht "IList" folgt. Stattdessen müssen Sie eine eigene "IListPersonnal" -Schnittstelle definieren, die ausnahmslos bei "Einfügen", "Entfernen" und zufälligem Zugriff (Indexer) fehlschlagen kann. Aber wer wird es verwenden wollen, wenn es nicht Standard ist?
quelle
ConcurrentOrderedBag<T>
die eine schreibgeschützte Implementierung von beinhaltetIList<T>
, aber auch eine vollständig threadsichereint Add(T value)
Methode bietet . Ich verstehe nicht, warumForEach
Änderungen erforderlich wären. Obwohl Microsoft dies nicht ausdrücklich sagt, deutet ihre Praxis darauf hin, dass es durchaus akzeptabel istIEnumerator<T>
, die zum Zeitpunkt der Erstellung vorhandenen Sammlungsinhalte aufzulisten. Die sammlungsmodifizierte Ausnahme ist nur erforderlich, wenn der Enumerator keinen störungsfreien Betrieb garantieren kann.GetEnumerator
Methode, eine Sammlung nach ihrer Rückkehr gesperrt zu lassen. Solche Designs können leicht zu einem Deadlock führen. DiesIEnumerable<T>
gibt keinen Hinweis darauf, ob erwartet werden kann, dass eine Aufzählung abgeschlossen wird, selbst wenn eine Sammlung geändert wird. Das Beste, was man tun kann, ist, seine eigenen Methoden so zu schreiben, dass sie dies tun, und Methoden zu haben, dieIEnumerable<T>
die Tatsache dokumentieren, dass sie nur dann threadsicher sind, wenn sieIEnumerable<T>
eine thread-sichere Aufzählung unterstützen.IEnumerable<T>
eine "Snapshot" -Methode mit Rückgabetyp enthalten gewesen wäreIEnumerable<T>
. Unveränderliche Sammlungen könnten sich selbst zurückgeben; Eine begrenzte Sammlung könnte sich, wenn nichts anderes, in einList<T>
oder kopierenT[]
und das aufrufenGetEnumerator
. Einige unbegrenzte Sammlungen könnten implementiert werdenSnapshot
, und diejenigen, die keine Ausnahme auslösen könnten , ohne zu versuchen, eine Liste mit ihrem Inhalt zu füllen.Bei der sequentiellen Ausführung von Code unterscheiden sich die verwendeten Datenstrukturen von (gut geschriebenen) gleichzeitig ausgeführten Codes. Der Grund ist, dass sequentieller Code implizite Reihenfolge impliziert. Gleichzeitiger Code impliziert jedoch keine Reihenfolge; Besser noch, es impliziert das Fehlen einer definierten Reihenfolge!
Aus diesem Grund sind Datenstrukturen mit impliziter Reihenfolge (wie List) für die Lösung gleichzeitiger Probleme nicht sehr nützlich. Eine Liste impliziert eine Reihenfolge, definiert jedoch nicht klar, um welche Reihenfolge es sich handelt. Aus diesem Grund bestimmt die Ausführungsreihenfolge des Codes, der die Liste manipuliert, (bis zu einem gewissen Grad) die implizite Reihenfolge der Liste, die in direktem Konflikt mit einer effizienten gleichzeitigen Lösung steht.
Denken Sie daran, dass Parallelität ein Datenproblem ist, kein Codeproblem! Sie können den Code nicht zuerst implementieren (oder vorhandenen sequentiellen Code neu schreiben) und erhalten eine gut gestaltete gleichzeitige Lösung. Sie müssen zuerst die Datenstrukturen entwerfen und dabei berücksichtigen, dass in einem gleichzeitigen System keine implizite Reihenfolge vorhanden ist.
quelle
Der sperrlose Kopier- und Schreibansatz funktioniert hervorragend, wenn Sie nicht mit zu vielen Elementen arbeiten. Hier ist eine Klasse, die ich geschrieben habe:
Anwendungsbeispiel: orders_BUY = CopyAndWriteList.Clear (orders_BUY);
quelle
Ich habe eine ähnliche wie Brian implementiert . Meins ist anders:
yield return
für die Erstellung eines Enumerators.DoSync
undGetSync
Methoden, die sequentielle Interaktionen ermöglichen, die exklusiven Zugriff auf die Liste erfordern.Der Code :
quelle
try
BlockanfangRemove
oder in den Indexer-Setter gelangen?IList
Semantik in gleichzeitigen Szenarien bestenfalls begrenzt ist. Ich habe diesen Code wahrscheinlich geschrieben, bevor ich zu dieser Erkenntnis kam. Meine Erfahrung ist die gleiche wie die des Autors der akzeptierten Antwort: Ich habe es mit dem versucht, was ich über Synchronisation und IList <T> wusste, und dadurch etwas gelernt.