Freigeben über


Verwendung einer threadsicheren Auflistung

Mit .NET Framework 4 wurden fünf neue Auflistungstypen eingeführt, die speziell für die Unterstützung von Hinzufügungs- und Entfernungsvorgängen in mehreren Threads konzipiert sind. Diese Typen verwenden unterschiedliche Arten effizienter sperrender und sperrfreier Synchronisierungsmechanismen, um die Threadsicherheit zu gewährleisten. Ein Vorgang wird durch Synchronisierung aufwändiger. Das Ausmaß des Aufwands hängt von der Art der verwendeten Synchronisierung, der Art der ausgeführten Vorgänge und von anderen Faktoren ab, z.B. der Anzahl der Threads, die versuchen, gleichzeitig auf die Sammlung zuzugreifen.

In einigen Szenarien ist der Synchronisierungsaufwand unwesentlich und ermöglicht eine deutlich schnellere Ausführung und bessere Skalierbarkeit des Multithreadtyps als beim nicht threadsicheren Äquivalent, wenn ein Schutz durch eine externe Sperre besteht. In anderen Szenarien kann der Aufwand dazu führen, dass der threadsichere Typ in etwa mit der gleichen oder sogar einer geringeren Leistung und Skalierbarkeit ausgeführt wird wie die extern gesperrte, nicht threadsichere Version des Typs.

Die folgenden Abschnitte enthalten eine allgemeine Anleitung dazu, wann eine threadsichere Sammlung anstelle ihres nicht threadsicheren Äquivalents verwendet wird, das eine vom Benutzer bereitgestellte Sperre für die Lese- und Schreibvorgänge besitzt. Da die Leistung von vielen Faktoren abhängig sein kann, handelt es sich nicht um eine spezielle Anleitung, die daher nicht unter allen Umständen gültig ist. Wenn die Leistung sehr wichtig ist, ist die am besten geeignete Methode zur Bestimmung des zu verwendenden Sammlungstyps die Messung der Leistung auf Basis von repräsentativen Computerkonfigurationen und Lasten. In diesem Dokument werden die folgenden Begriffe verwendet:

Reines Producer-Consumer-Szenario
In jedem vorhandenen Thread werden Elemente entweder hinzugefügt oder entfernt, es finden jedoch nicht beide Vorgänge statt.

Gemischtes Producer-Consumer-Szenario
In jedem vorhandenen Thread werden Elemente sowohl hinzugefügt als auch entfernt.

Geschwindigkeitszuwachs
Höhere algorithmische Leistung relativ zu einem anderen Typ im gleichen Szenario.

Skalierbarkeit
Die Zunahme der Leistung, die proportional zur Anzahl der Kerne des Computers ist. Mit einem Algorithmus, der skaliert wird, werden bei acht Kernen höhere Leistungen erzielt als bei zwei Kernen.

ConcurrentQueue(T) oder Queue(T)

In reinen Producer-Consumer-Szenarien, in denen die Verarbeitungszeit für jedes Element sehr kurz ist (einige Anweisungen), kann System.Collections.Concurrent.ConcurrentQueue<T> geringfügige Leistungsvorteile gegenüber System.Collections.Generic.Queue<T> mit einer externen Sperre bieten. In diesem Szenario erzielt ConcurrentQueue<T> die beste Leistung, wenn sich ein dedizierter Thread in der Warteschlange befindet und ein dedizierter Thread die Warteschlange verlässt. Wenn Sie diese Regel nicht erzwingen, kann Queue<T> sogar etwas schneller als ConcurrentQueue<T> auf Computern mit mehreren Kernen ausgeführt werden.

Wenn die Verarbeitungszeit bei etwa 500 FLOPS (Gleitkommavorgänge) oder höher liegt, gilt die Zwei-Thread-Regel nicht für ConcurrentQueue<T>, sodass dann eine sehr gute Skalierbarkeit möglich ist. Queue<T> lässt sich in diesem Szenario nicht vorteilhaft skalieren.

Bei sehr geringer Verarbeitungszeit zeichnet sich Queue<T> mit einer externen Sperre in gemischten Producer-Consumer-Szenarien durch eine bessere Skalierbarkeit als ConcurrentQueue<T> aus. Wenn die Verarbeitungszeit jedoch bei etwa 500 FLOPS oder darüber liegt, kann ConcurrentQueue<T> besser skaliert werden.

ConcurrentStack oder Stapel

In reinen Producer-Consumer-Szenarien erzielen System.Collections.Concurrent.ConcurrentStack<T> und System.Collections.Generic.Stack<T> mit einer externen Sperre bei sehr geringer Verarbeitungszeit wahrscheinlich annähernd die gleiche Leistung mit einem dedizierten Thread für Ablegevorgänge und einem dedizierten Thread für Abholvorgänge. Bei zunehmender Anzahl der Threads werden jedoch beide Typen aufgrund des stärkeren Konflikts langsamer, und mit Stack<T> werden unter Umständen bessere Leistungen als mit ConcurrentStack<T> erzielt. Wenn die Verarbeitungszeit bei rund 500 FLOPS oder darüber liegt, werden beide Typen mit der etwa gleichen Rate skaliert.

In gemischten Producer-Consumer-Szenarien ist ConcurrentStack<T> für kleine und große Arbeitsauslastungen schneller.

Die Verwendung von PushRange und TryPopRange kann die Zugriffszeiten unter Umständen erheblich beschleunigen.

ConcurrentDictionary oder Dictionary

Verwenden Sie System.Collections.Concurrent.ConcurrentDictionary<TKey,TValue> im Allgemeinen in jedem Szenario, in dem Sie Schlüssel oder Werte gleichzeitig aus mehreren Threads hinzufügen und aktualisieren. In Szenarien, die häufige Updates und relativ wenige Lesevorgänge umfassen, bietet ConcurrentDictionary<TKey,TValue> in der Regel geringfügige Vorteile. In Szenarien, die zahlreiche Lesevorgänge und Updates umfassen, ist ConcurrentDictionary<TKey,TValue> im Allgemeinen auf Computern bedeutend schneller, die über eine beliebige Anzahl von Kernen verfügen.

In Szenarien, die häufige Updates umfassen, können Sie den Grad der Parallelität in ConcurrentDictionary<TKey,TValue> erhöhen und anschließend ermitteln, ob sich die Leistung auf Computern mit einer größeren Anzahl von Kernen verbessert. Wenn Sie die Parallelitätsebene ändern, vermeiden Sie so weit wie möglich globale Vorgänge.

Wenn Sie nur Schlüssel oder Werte lesen, ist Dictionary<TKey,TValue> schneller, da keine Synchronisierung erforderlich ist, wenn das Wörterbuch nicht von Threads geändert wird.

ConcurrentBag

In reinen Producer-Consumer-Szenarien ist System.Collections.Concurrent.ConcurrentBag<T> wahrscheinlich langsamer als die anderen gleichzeitigen Sammlungstypen.

In gemischten Producer-Consumer-Szenarien ist ConcurrentBag<T> sowohl bei großen als auch bei kleinen Arbeitsauslastungen im Allgemeinen viel schneller und besser skalierbar als ein beliebiger anderer gleichzeitiger Sammlungstyp.

BlockingCollection

Wenn Begrenzungs- und Blockierungssemantiken erforderlich sind, wird System.Collections.Concurrent.BlockingCollection<T> wahrscheinlich schneller als jede beliebige benutzerdefinierte Implementierung ausgeführt. Zudem werden umfassende Möglichkeiten für die Abbruch-, Enumerations- und Ausnahmebehandlung unterstützt.

Siehe auch