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.