Das .NET Framework stellt threadsichere Collections im Namespace System.Collections.Concurrent bereit. Zu den bekannten Collections Stack<T>, Queue<T> und Dictionary<TKey,TValue> existieren in diesem Namespace entsprechende Äquivalente:
ConcurrentStack<T>
ConcurrentQueue<T>
ConcurrentDictionary<TKey,TValue>
Zusätzlich wird eine ConcurrentBag<T> Collection angeboten.
In diesem Artikel möchte ich Ihnen diese vier Concurrent Collections vorstellen.
Threadsicherheit vs. Geschwindigkeit
Die Concurrent Collections sind für den Einsatz in Multithreading Anwendungen optimiert. Sie sind dadurch langsamer und benötigen meist auch mehr Speicher als die klassischen Collections. Beispielsweise sind ConcurrentStack und ConcurrentQueue intern als verkettete Liste implementiert. Im Vergleich zu den Stack und Queue Klassen sind diese daher langsamer und speicherintensiver aber im Gegenzug besser geeignet für einen threadsicheren Zugriff.
Interne vs. externe Threadsicherheit
Des Weiteren gilt es zu beachten, dass bei der Anwendung der Concurrent Collections keine falsche Sicherheit beim Entwickler aufkommen darf. Die Concurrent Collections an sich sind zwar threadsicher implementiert, der Code der diese benutzt muss aber selbstverständlich auch threadsicher geschrieben werden.
Stellen Sie sich dazu folgendes Beispielszenario vor. Ein Thread iteriert über eine Concurrent Collection um deren Werte in einer Anwendung auszugeben. Ein weiterer paralleler Thread verändert aber währenddessen die Werte der Concurrent Collection. Intern ist diese threadsicher implementiert wodurch dieser parallele Zugriff funktioniert. Es wird also kein Fehler geworfen. Die Ausgabe in der Anwendung würde in diesem Fall aber alte Werte und die aktuell geänderten Werte ausgeben. Solch ein Verhalten ist meist nicht gewünscht.
Als Entwickler muss man sich daher zwar keine Gedanken um die interne Threadsicherheit der Concurrent Collections machen, sich aber um die externe Threadsicherheit des umgebenden Programmcodes kümmern.
ConcurrentStack, ConcurrentQueue, ConcurrentBag
Die drei Concurrent Collections ConcurrentStack, ConcurrentQueue und ConcurrentBag stellen einfache Listen zum Verwalten von Elementen bereit. Die Elemente lassen sich dabei zu den Listen hinzufügen und aus diesen wieder ermitteln und entfernen. Die Abfrage ist dabei abhängig von der gewählten Art der Concurrent Collection.
Bei der Abfrage und dem damit verbundenen entfernen eines Elements aus der Liste wird jeweils folgendes Element geliefert:
- ConcurrentStack: Es wird das aktuellste Element abgefragt, also jenes welches als letztes hinzugefügt wurde. (Last In/First Out Prinzip)
- ConcurrentQueue: Es wird das älteste Element abgefragt, also jenes welches als erstes hinzugefügt wurde. (First In/First Out Prinzip)
- ConcurrentBag: Es wird ein beliebiges Element abgefragt.
ConcurrentBag gibt bei einer Abfrage ein aus Sicht des Anwenders zufälliges Element zurück. Dabei wird aber nicht nach dem Zufallsprinzip gehandelt, sondern es findet eine geschwindigkeitsoptimierte Abfrage statt. Die ConcurrentBag Collection gibt somit immer das Element zurück welches am effizientesten ermittelt werden kann.
ConcurrentDictionary
Ein ConcurrentDictionary verhält sich genau wie ein normales Dictionary. Das heisst, die Elemente werden mittels eines Keys verwaltet. Dieser Key wird beim Hinzufügen und Abfragen eines Elementes angegeben.
Beispiel
Das nachfolgende Beispiel zeigt eine Konsolenanwendung in welcher ein ConcurrentBag verwendet wird. Zuerst werden Werte mittels zwei paralleler Threads hinzugefügt und danach mittels zwei paralleler Threads ausgelesen.
class Program { static void Main(string[] args) { Parallel.Invoke(AddSomeItems, AddMoreItems); Parallel.Invoke(GetItemsThreadA, GetItemsThreadB); Console.ReadKey(); } static void AddSomeItems() { for (int i = 1; i <= 5; i++) { _bag.Add(i); } } static void AddMoreItems() { for (int i = 11; i <= 15; i++) { _bag.Add(i); } } static void GetItemsThreadA() { int item; while (true == _bag.TryTake(out item)) { Console.WriteLine("Thread A: " + item.ToString()); Thread.Sleep(50); } } static void GetItemsThreadB() { int item; while (true == _bag.TryTake(out item)) { Console.WriteLine("Thread B: " + item.ToString()); Thread.Sleep(50); } } private static ConcurrentBag<int> _bag = new ConcurrentBag<int>(); }
Das Programm hat folgende Ausgabe auf der Konsole erzeugt. Bitte beachten Sie, dass die Ausgabe bei jedem Programmstart anders sein kann.
Thread A: 15
Thread B: 1
Thread A: 14
Thread B: 2
Thread A: 13
Thread B: 3
Thread A: 12
Thread B: 4
Thread B: 5
Thread A: 11
Fazit
Sollen Collections in einer Multithreading-Applikation eingesetzt werden dann bieten sich die Concurrent Collections an. Diese stellen eine threadsicher Alternative zu den Standard Collections dar.