Kdy použít kolekci bezpečnou pro přístup z více vláken

Rozhraní .NET Framework 4 zavedlo pět typů kolekcí, které jsou speciálně navržené tak, aby podporovaly operace přidání a odebrání více vláken. K zajištění bezpečnosti vláken tyto typy používají různé druhy účinných mechanismů uzamčení a synchronizace bez zámků. Synchronizace přidává režii k operaci. Množství režijních nákladů závisí na použitém typu synchronizace, druhu provedených operací a dalších faktorech, jako je počet vláken, která se pokoušejí o souběžný přístup k kolekci.

V některých scénářích je režie synchronizace zanedbatelná a umožňuje vícevláknovým typům provádět výrazně rychlejší a škálovat mnohem lépe než jeho nevláknové ekvivalenty, pokud je chráněný externím zámkem. V jiných scénářích může režie způsobit, že typ bezpečný pro přístup z více vláken provede a škáluje se o stejné nebo ještě pomaleji než externě uzamčená verze typu, která není bezpečná pro přístup z více vláken.

V následujících částech najdete obecné pokyny, kdy použít kolekci bezpečnou pro přístup z více vláken oproti jejímu nevláknově bezpečnému ekvivalentu, který má uzamčený uživatelem kolem operací čtení a zápisu. Vzhledem k tomu, že se výkon může lišit v závislosti na mnoha faktorech, pokyny nejsou specifické a nemusí být nutně platné za všech okolností. Pokud je výkon velmi důležitý, nejlepší způsob, jak určit, který typ kolekce se má použít, je měřit výkon na základě reprezentativních konfigurací a zatížení počítače. Tento dokument používá následující termíny:

Scénář čistě producent-spotřebitel
Jakékoli dané vlákno buď přidává nebo odebírá prvky, ale ne oba.

Scénář smíšeného producenta a příjemce
Každé dané vlákno přidává i odebírá prvky.

Zrychlení
Rychlejší algoritmický výkon vzhledem k jinému typu ve stejném scénáři

Škálovatelnost
Zvýšení výkonu, které je úměrné počtu jader v počítači. Algoritmus, který se škáluje, funguje rychleji na osmi jádrech než na dvou jádrech.

ConcurrentQueue(T) vs. Queue(T)

Ve scénářích čistě producent-spotřebitel, kde je doba zpracování pro každý prvek velmi malá (několik pokynů), pak System.Collections.Concurrent.ConcurrentQueue<T> může nabídnout skromné výhody výkonu oproti System.Collections.Generic.Queue<T> externímu zámku. V tomto scénáři je nejlepší, ConcurrentQueue<T> když je jedno vyhrazené vlákno ve frontě a jedno vyhrazené vlákno je de-queuing. Pokud toto pravidlo nevynucujete, Queue<T> může to být i trochu rychlejší než ConcurrentQueue<T> v počítačích s více jádry.

Pokud je doba zpracování přibližně 500 FLOPS (operace s plovoucí desetinnou čárkou) nebo více, pravidlo dvou vláken se nevztahuje na ConcurrentQueue<T>, což má velmi dobrou škálovatelnost. Queue<T> není v tomto scénáři správně škálovat.

Ve scénářích se smíšenými producenty, kdy je doba zpracování velmi malá, Queue<T> se škáluje lépe než ConcurrentQueue<T> externí zámek. Pokud je však doba zpracování přibližně 500 FLOPS nebo více, ConcurrentQueue<T> pak se škáluje lépe.

ConcurrentStack vs. Stack

Ve scénářích čistě producent-příjemce, když je doba zpracování velmi malá, pak System.Collections.Concurrent.ConcurrentStack<T> a System.Collections.Generic.Stack<T> to má externí zámek bude pravděpodobně fungovat přibližně stejně s jedním vyhrazeným nasdílením vlákna a jedním vyhrazeným skákajícím vláknem. S rostoucím počtem vláken se však oba typy zpomalují kvůli zvýšené kolizí a Stack<T> můžou fungovat lépe než ConcurrentStack<T>. Pokud je doba zpracování přibližně 500 FLOPS nebo více, oba typy se škálují přibližně stejnou rychlostí.

Ve scénářích se smíšenými producenty ConcurrentStack<T> je pro malé i velké úlohy rychlejší.

Použití PushRange a TryPopRange může výrazně urychlit dobu přístupu.

ConcurrentDictionary vs. Dictionary

Obecně platí, že v System.Collections.Concurrent.ConcurrentDictionary<TKey,TValue> jakémkoli scénáři, ve kterém přidáváte a aktualizujete klíče nebo hodnoty souběžně z více vláken. Ve scénářích, které zahrnují časté aktualizace a relativně málo čtení, ConcurrentDictionary<TKey,TValue> obecně nabízí skromné výhody. Ve scénářích, které zahrnují mnoho čtení a mnoho aktualizací, ConcurrentDictionary<TKey,TValue> je obecně výrazně rychlejší na počítačích, které mají libovolný počet jader.

Ve scénářích, které zahrnují časté aktualizace, můžete zvýšit stupeň souběžnosti a ConcurrentDictionary<TKey,TValue> pak změřit, jestli se výkon zvýší na počítačích s více jádry. Pokud změníte úroveň souběžnosti, vyhněte se globálním operacím co nejvíce.

Pokud čtete jenom klíč nebo hodnoty, je rychlejší Dictionary<TKey,TValue> , protože není nutná žádná synchronizace, pokud slovník není upraven žádnými vlákny.

Concurrentbag

Ve scénářích System.Collections.Concurrent.ConcurrentBag<T> čistě producent-příjemce bude pravděpodobně provádět pomaleji než ostatní souběžné typy kolekcí.

Ve scénářích se smíšenými producenty ConcurrentBag<T> je obecně mnohem rychlejší a škálovatelnější než jakýkoli jiný souběžný typ kolekce pro velké i malé úlohy.

BlockingCollection

Při ohraničování a blokování sémantiky System.Collections.Concurrent.BlockingCollection<T> se pravděpodobně bude provádět rychleji než jakákoli vlastní implementace. Podporuje také bohaté zrušení, výčet a zpracování výjimek.

Viz také