Ahhoz, hogy hatékonyan tudjunk párhuzamos és többszálú kóddal dolgozni, olyan adatstruktúrákra és kollekciókra van szükségünk, amelyek szálbiztosak. A .NET 4.0 óta lehetőségünk van ilyenek használatára a keretrendszerben anélkül, hogy magunknak kellene megpróbálkozni ilyenek írásával.
Leginkább négy fajta kollekciót érdemes megemlíteni, a ConcurrentQueue<T> és ConcurrentStack<T> a nevükből adódóan a sor és a verem szálbiztos verziói, a ConcurrentDictionary<TKey, TValue> pedig a szótáré. Kissé elüt a ConcurrentBag<T>, ami igazából egy rendezetlen listája, vagy szakszóval kupaca az elemeknek.
Implementációs különbség is van az első kettő és az utolsó kettő között, mégpedig az, hogy a sor és verem implementációik Interlocked operációkkal dolgoznak, míg az utóbbi kettő valamilyen lockot használ. Van egy ötödik fajta kollekció is, a BlockingCollection<T> is, de az leginkább egy wrapperként működik a már említett típusok felett.
A foreach ciklussal való bejáráskor a legtöbb, fent említett kollekció lemásolódik, ezzel biztosítva, hogy ne okozzon gondot, ha egy másik szál éppen hozzáadna, vagy elvenne elemet a belőlük.
ConcurrentQueue<T> és ConcurrentStack<T>
A két osztály gyakorlatilag a Queue<T> és a Stack<T> szálbiztos megoldása.
A ConcurrentStack rendelkezik egy Push és egy TryPop metódussal, amelyekkel egy-egy elem adható hozzá vagy vehető ki a veremből.
Ha több elemet szeretnénk beszúrni vagy kivenni, akkor a PushRange és a TryPopRange metódusok állnak a rendelkezésünkre.
Minden elem törlésére a Clear eljárás alkalmas, míg azt, hogy üres-e, az IsEmpty tulajdonság mondja meg.
A ConcurrentQueue nagyon hasonló, csak itt Enqueue és TryDequeue használatos a sorba beillesztésre és a sorból kivételre.
Ha megnézzük a két osztály forráskódját, akkor látható, hogy mindkét esetben láncolt listával van dolgunk. Ez nyilván triviális is, mivel mindkét esetben csak a lista elejével kell foglalkoznunk, de olyan dolgok is megoldottak, mint az indexelés. A .NET Framework 4.6-os verziója óta mindkét osztály megvalósítja az IReadOnlyCollection<T> interfészt is.
ConcurrentBag<T>
Mint már szó volt róla, ez egy rendezetlen gyűjtemény. Nincs a tartalomnak sorrendje, így az sincs megkötve, hogy minden elem csak egyszer szerepelhessen.
Ami számunkra érdekes, az az Add és a TryTake metódus. Az egyik hozzáad egy elemet, a másik kiveszi azt. Csak persze nem tudjuk a sorrendet, úgyhogy ezt akkor érdemes használni, ha teljesen mindegy, melyik példányt kapjuk meg. Mondjuk akkor, ha egy ThreadPool implementációt akarunk írni. Ez a típus is implementálja az IReadOnlyCollection<T> interfészt.
ConcurrentDictionary<TKey, TValue>
A szálbiztos szótár az IDictionary és IDictionary<TKey, TValue> interfészeket implementálja. A főbb metódusai az AddOrUpdate, a GetOrAdd, illetve ezek Try szóval kezdődő verziói, vagyis a TryAdd, TryUpdate, TryGet és TryRemove.
Korábban már volt róla szó, hogy a ConcurrentDictionary atomi műveletek helyett monitorokkal dolgozik. Olyannyira, hogy több lock object is van minden példányban.
Mivel belül van megoldva a szinkronizáció, ezért nekünk már nem kell a használatnál erről gondoskodni. Ez viszont azt eredményezi, hogy például a GetOrAdd és AddOrUpdate bizonyos esetekben nem úgy fog működni, ahogy azt elvárnánk. Ezt mindenképp figyelembe kell venni a tervezésnél.
Gyakorlati hasznuk
A gyakorlatban a ConcurrentQueue<T>, a ConcurrentStack<T> illetve a BlockingCollection<T> kollekciók kiválóan alkalmasak termelő-fogyasztó (producer-consumer) minta megvalósításához, tehát olyan esetben, ahol az adatok kollekcióba történő ki-, illetve bekerülése több, egymástól független szálon történik.
A BlockingCollection<T> egy különösen jó választás lehet, főleg amikor a termelők, vagy a fogyasztók gyorsabbak és nem baj, ha bármelyik fél blokkolódik. Amennyiben elfogyott benne a feldolgozandó adat, akkor blokkolja a fogyasztókat, amikor ki próbálnak venni belőle adatot. Megadható maximum méret is, amennyiben ezt elérte, automatikusan blokkolja a termelőket, amikor új példányt adnának a kollekcióhoz. Ez akkor nagyon hasznos, ha a termelők gyorsabban és nagy adatot termelhetnek, és ha nem blokkolnának, a kollekció betelíthetné a folyamat rendelkezésére álló memóriát.