Kiedy należy używać kolekcji bezpiecznej wątkowo

Program .NET Framework 4 wprowadził pięć typów kolekcji, które są specjalnie zaprojektowane do obsługi wielowątkowych operacji dodawania i usuwania. Aby zapewnić bezpieczeństwo wątków, te typy używają różnych rodzajów wydajnych mechanizmów synchronizacji blokady i bez blokady. Synchronizacja dodaje obciążenie do operacji. Ilość obciążeń zależy od rodzaju używanej synchronizacji, rodzaju wykonywanych operacji i innych czynników, takich jak liczba wątków, które próbują jednocześnie uzyskać dostęp do kolekcji.

W niektórych scenariuszach obciążenie synchronizacji jest niewielkie i umożliwia wykonywanie znacznie szybszego działania typu wielowątkowego i skalowanie znacznie lepsze niż jego odpowiednik bezpieczny dla wątków, gdy jest chroniony przez blokadę zewnętrzną. W innych scenariuszach obciążenie może spowodować, że typ bezpieczny wątkowo będzie działać i skalować mniej więcej tak samo, a nawet wolniej niż zewnętrznie zablokowana, niewątkowa wersja typu.

W poniższych sekcjach przedstawiono ogólne wskazówki dotyczące sposobu używania kolekcji bezpiecznej wątkowo w porównaniu z bezpiecznym dla wątków odpowiednikiem, który ma blokadę zapewnianą przez użytkownika wokół operacji odczytu i zapisu. Ponieważ wydajność może się różnić w zależności od wielu czynników, wskazówki nie są specyficzne i niekoniecznie są prawidłowe we wszystkich okolicznościach. Jeśli wydajność jest bardzo ważna, najlepszym sposobem określenia typu kolekcji do użycia jest mierzenie wydajności na podstawie reprezentatywnych konfiguracji i obciążeń komputerów. W tym dokumencie są używane następujące terminy:

Czysty scenariusz producenta-konsumenta
Każdy dany wątek jest albo dodawać lub usuwać elementy, ale nie oba.

Scenariusz mieszany producent-konsument
Każdy dany wątek dodaje i usuwa elementy.

Przyspieszenie
Szybsza wydajność algorytmiczna względem innego typu w tym samym scenariuszu.

Skalowalność
Wzrost wydajności proporcjonalny do liczby rdzeni na komputerze. Algorytm, który skaluje, działa szybciej na ośmiu rdzeniach niż na dwóch rdzeniach.

ConcurrentQueue(T) a Queue(T)

W czystych scenariuszach dla konsumentów producentów, w których czas przetwarzania dla każdego elementu jest bardzo mały (kilka instrukcji), a następnie System.Collections.Concurrent.ConcurrentQueue<T> może oferować niewielkie korzyści z wydajności w odniesieniu do blokady System.Collections.Generic.Queue<T> zewnętrznej. W tym scenariuszu sprawdza się najlepiej, ConcurrentQueue<T> gdy jeden dedykowany wątek jest kolejkujący, a jeden dedykowany wątek jest od kolejkowania. Jeśli ta reguła nie zostanie wymuszona, może nawet Queue<T> działać nieco szybciej niż ConcurrentQueue<T> na komputerach z wieloma rdzeniami.

Gdy czas przetwarzania wynosi około 500 operacji FLOPS (operacje zmiennoprzecinkowe) lub więcej, reguła dwuwątkowa nie ma zastosowania do ConcurrentQueue<T>elementu , który następnie ma bardzo dobrą skalowalność. Queue<T> nie jest dobrze skalowana w tym scenariuszu.

W scenariuszach mieszanych producentów-konsumentów, gdy czas przetwarzania jest bardzo mały, wartość Queue<T> , która ma zewnętrzne skalowanie blokady lepiej niż ConcurrentQueue<T> to robi. Jednak w przypadku, gdy czas przetwarzania wynosi około 500 FLOPS lub więcej, ConcurrentQueue<T> skalowanie jest lepsze.

ConcurrentStack a Stack

W czystych scenariuszach konsumenckich producentów, gdy czas przetwarzania jest bardzo mały, a następnie System.Collections.Concurrent.ConcurrentStack<T>System.Collections.Generic.Stack<T> ma blokadę zewnętrzną prawdopodobnie będzie działać mniej więcej tak samo z jednym dedykowanym wątkiem wypychania i jednym dedykowanym wątkiem popping. Jednak w miarę wzrostu liczby wątków oba typy spowalniają z powodu zwiększonej rywalizacji i Stack<T> mogą działać lepiej niż ConcurrentStack<T>. Gdy czas przetwarzania wynosi około 500 FLOPS lub więcej, oba typy są skalowane w mniej więcej tym samym tempie.

W scenariuszach mieszanych ConcurrentStack<T> producentów i konsumentów szybsze jest zarówno w przypadku małych, jak i dużych obciążeń.

Korzystanie z elementów PushRange i TryPopRange może znacznie przyspieszyć czasy dostępu.

ConcurrentDictionary a Słownik

Ogólnie rzecz biorąc, należy użyć elementu System.Collections.Concurrent.ConcurrentDictionary<TKey,TValue> w dowolnym scenariuszu, w którym dodajesz i aktualizujesz klucze lub wartości współbieżnie z wielu wątków. W scenariuszach obejmujących częste aktualizacje i stosunkowo mało odczytów, ConcurrentDictionary<TKey,TValue> ogólnie oferuje skromne korzyści. W scenariuszach obejmujących wiele operacji odczytu i wielu aktualizacji na komputerach, które mają dowolną liczbę rdzeni, ConcurrentDictionary<TKey,TValue> jest to znacznie szybsze.

W scenariuszach, które obejmują częste aktualizacje, można zwiększyć stopień współbieżności w ConcurrentDictionary<TKey,TValue> obiekcie , a następnie zmierzyć, czy wzrost wydajności na komputerach, które mają więcej rdzeni. Jeśli zmienisz poziom współbieżności, unikaj jak największej ilości operacji globalnych.

Jeśli odczytujesz tylko klucz lub wartości, funkcja jest szybsza, ponieważ synchronizacja nie jest wymagana, Dictionary<TKey,TValue> jeśli słownik nie jest modyfikowany przez żadne wątki.

Concurrentbag

W przypadku czystych scenariuszy konsumenckich System.Collections.Concurrent.ConcurrentBag<T> producentów prawdopodobnie będzie działać wolniej niż inne współbieżne typy kolekcji.

W scenariuszach mieszanych ConcurrentBag<T> producentów i konsumentów jest ogólnie znacznie szybsze i bardziej skalowalne niż jakikolwiek inny współbieżny typ kolekcji zarówno dla dużych, jak i małych obciążeń.

BlockingCollection

W przypadku konieczności System.Collections.Concurrent.BlockingCollection<T> ograniczenia i blokowania semantyki prawdopodobnie będzie działać szybciej niż jakakolwiek implementacja niestandardowa. Obsługuje również zaawansowane anulowanie, wyliczenie i obsługę wyjątków.

Zobacz też