.NET - blokowanie słownika a ConcurrentDictionary


133

Nie mogłem znaleźć wystarczających informacji o ConcurrentDictionarytypach, więc pomyślałem, że zapytam o to tutaj.

Obecnie używam a, Dictionaryaby zatrzymać wszystkich użytkowników, do których stale uzyskuje dostęp wiele wątków (z puli wątków, więc nie ma dokładnej liczby wątków) i ma zsynchronizowany dostęp.

Niedawno dowiedziałem się, że w .NET 4.0 był zestaw bezpiecznych wątków i wydaje się to być bardzo przyjemne. Zastanawiałem się, jaka byłaby opcja `` bardziej wydajna i łatwiejsza w zarządzaniu '', ponieważ mam opcję między posiadaniem normalnego Dictionaryz zsynchronizowanym dostępem lub posiadaniem tego, ConcurrentDictionaryktóry jest już bezpieczny dla wątków.

Odniesienie do .NET 4.0 ConcurrentDictionary


Odpowiedzi:


148

Kolekcja bezpieczna wątkowo w porównaniu z kolekcją bez ochrony wątków może być postrzegana w inny sposób.

Rozważmy sklep bez sprzedawcy, z wyjątkiem kasy. Masz mnóstwo problemów, jeśli ludzie nie działają odpowiedzialnie. Na przykład, powiedzmy, że klient bierze puszkę z piramidy - może, podczas gdy urzędnik obecnie buduje piramidę, rozpętałoby się piekło. A co, jeśli dwóch klientów sięgnie po ten sam przedmiot w tym samym czasie, kto wygrywa? Czy będzie walka? To jest kolekcja bez ochrony wątków. Istnieje wiele sposobów na uniknięcie problemów, ale wszystkie wymagają pewnego rodzaju blokowania lub raczej jawnego dostępu w taki czy inny sposób.

Z drugiej strony, rozważ sklep z urzędnikiem przy biurku, a zakupy możesz robić tylko za jego pośrednictwem. Stajesz w kolejce i prosisz go o przedmiot, on przynosi ci go z powrotem, a ty wychodzisz z kolejki. Jeśli potrzebujesz wielu przedmiotów, możesz odebrać tylko tyle przedmiotów w każdej podróży w obie strony, ile pamiętasz, ale musisz uważać, aby nie zapychać urzędnika, to złości innych klientów w kolejce za tobą.

Teraz rozważ to. W sklepie z jednym urzędnikiem, co, jeśli dojdziesz do pierwszej linii i zapytasz sprzedawcę „Czy masz papier toaletowy”, a on powie „Tak”, a potem powiesz „Ok, ja” Skontaktuję się z Tobą, kiedy będę wiedział, ile potrzebuję ”, a gdy wrócisz na początek kolejki, sklep może oczywiście zostać wyprzedany. Ten scenariusz nie jest chroniony przez kolekcję z ochroną wątków.

Kolekcja z ochroną wątków gwarantuje, że jej wewnętrzne struktury danych są zawsze prawidłowe, nawet jeśli uzyskiwany jest do nich dostęp z wielu wątków.

Kolekcja bez ochrony wątków nie jest objęta żadnymi takimi gwarancjami. Na przykład, jeśli dodasz coś do drzewa binarnego w jednym wątku, podczas gdy inny wątek jest zajęty równoważeniem drzewa, nie ma gwarancji, że element zostanie dodany, a nawet, że drzewo będzie nadal ważne później, może być uszkodzone bez nadziei.

Kolekcja z ochroną wątków nie gwarantuje jednak, że wszystkie sekwencyjne operacje w wątku działają na tej samej „migawce” wewnętrznej struktury danych, co oznacza, że ​​jeśli masz taki kod:

if (tree.Count > 0)
    Debug.WriteLine(tree.First().ToString());

możesz otrzymać NullReferenceException, ponieważ pomiędzy tree.Counta tree.First()inny wątek wyczyścił pozostałe węzły w drzewie, co oznacza, First()że powróci null.

W tym scenariuszu musisz albo sprawdzić, czy dana kolekcja ma bezpieczny sposób na uzyskanie tego, czego chcesz, być może musisz przepisać powyższy kod lub może być konieczne zablokowanie.


9
Za każdym razem, gdy czytam ten artykuł, mimo że to wszystko prawda, w jakiś sposób podążamy złą drogą.
scope_creep,

19
Głównym problemem w aplikacji obsługującej wątki są zmienne struktury danych. Jeśli możesz tego uniknąć, zaoszczędzisz sobie kłopotów.
Lasse V. Karlsen

92
To fajne wyjaśnienie bezpieczeństwa wątków, jednak nie mogę pomóc, ale czuję, że tak naprawdę nie rozwiązało to pytania OP. To, o co zapytał OP (i co później natknąłem się na to pytanie), to różnica między używaniem standardu Dictionaryi samodzielną obsługą blokowania a używaniem ConcurrentDictionarytypu wbudowanego w .NET 4+. Właściwie jestem trochę zaskoczony, że zostało to zaakceptowane.
mclark1129

4
Bezpieczeństwo wątków to coś więcej niż tylko użycie odpowiedniej kolekcji. Korzystanie z odpowiedniej kolekcji to początek, a nie jedyna rzecz, z którą musisz się uporać. Myślę, że właśnie to chciał wiedzieć OP. Nie mogę zgadnąć, dlaczego to zostało zaakceptowane, minęło trochę czasu, odkąd to napisałem.
Lasse V. Karlsen

2
Myślę, że @ LasseV.Karlsen próbuje powiedzieć, że ... bezpieczeństwo wątków jest łatwe do osiągnięcia, ale musisz zapewnić poziom współbieżności w sposobie radzenia sobie z tym bezpieczeństwem. Zadaj sobie pytanie: „Czy mogę wykonać więcej operacji w tym samym czasie, zachowując bezpieczeństwo wątków obiektu?” Rozważmy następujący przykład: dwóch klientów chce zwrócić różne towary. Kiedy jako menadżer udajesz się w podróż w celu uzupełnienia zapasów w sklepie, czy nie możesz po prostu uzupełnić zapasów obu produktów podczas jednej podróży i nadal obsługiwać żądania obu klientów, czy też zamierzasz odbyć podróż za każdym razem, gdy klient zwróci towar?
Alexandru

69

Nadal musisz zachować ostrożność podczas korzystania z kolekcji bezpiecznych wątkowo, ponieważ bezpieczeństwo wątków nie oznacza, że ​​możesz zignorować wszystkie problemy z wątkami. Gdy kolekcja reklamuje się jako bezpieczna dla wątków, zwykle oznacza to, że pozostaje w spójnym stanie, nawet gdy wiele wątków odczytuje i zapisuje jednocześnie. Nie oznacza to jednak, że pojedynczy wątek zobaczy „logiczną” sekwencję wyników, jeśli wywoła wiele metod.

Na przykład, jeśli najpierw sprawdzisz, czy klucz istnieje, a następnie uzyskasz wartość odpowiadającą kluczowi, ten klucz może już nie istnieć nawet w wersji ConcurrentDictionary (ponieważ inny wątek mógł usunąć klucz). Nadal musisz użyć blokowania w tym przypadku (lub lepiej: połączyć dwa wywołania przy użyciu TryGetValue ).

Więc używaj ich, ale nie myśl, że daje to darmową przepustkę do zignorowania wszystkich problemów z współbieżnością. Nadal musisz być ostrożny.


4
Więc w zasadzie mówisz, że jeśli nie użyjesz obiektu poprawnie, to nie zadziała?
ChaosPandion

3
Zasadniczo musisz użyć TryAdd zamiast zwykłych Contains -> Add.
ChaosPandion

3
@Bruno: Nie. Jeśli nie zablokowałeś, inny wątek mógł usunąć klucz między dwoma połączeniami.
Mark Byers

13
Nie chcę tutaj brzmieć krótko, ale TryGetValuezawsze było częścią Dictionaryi zawsze było zalecanym podejściem. Ważnymi „współbieżnymi” metodami, które ConcurrentDictionarywprowadza, są AddOrUpdatei GetOrAdd. Dobra odpowiedź, ale mogłem wybrać lepszy przykład.
Aaronaught

4
Racja - w przeciwieństwie do bazy danych zawierającej transakcje, w większości współbieżnych struktur wyszukiwania w pamięci brakuje koncepcji izolacji , gwarantują one jedynie poprawność wewnętrznej struktury danych.
Chang

40

Wewnętrznie ConcurrentDictionary używa oddzielnej blokady dla każdego zasobnika mieszania. Tak długo, jak używasz tylko Add / TryGetValue i podobnych metod, które działają na pojedynczych wpisach, słownik będzie działał jako prawie pozbawiona blokad struktura danych z odpowiednią zaletą wydajności. OTOH metody wyliczania (w tym właściwość Count) blokują wszystkie zasobniki naraz i dlatego są gorsze niż zsynchronizowany słownik pod względem wydajności.

Powiedziałbym, po prostu użyj ConcurrentDictionary.


15

Myślę, że metoda ConcurrentDictionary.GetOrAdd jest dokładnie tym, czego potrzebuje większość scenariuszy wielowątkowych.


1
Użyłbym również TryGet, w przeciwnym razie marnujesz wysiłek na inicjowanie drugiego parametru do GetOrAdd za każdym razem, gdy klucz już istnieje.
Jorrit Schippers,

7
GetOrAdd ma przeciążenie GetOrAdd (TKey, Func <TKey, TValue>) , więc drugi parametr może być inicjowany z opóźnieniem tylko w przypadku, gdy klucz nie istnieje w słowniku. Poza tym TryGetValue służy do pobierania danych, a nie ich modyfikowania. Kolejne wywołania każdej z tych metod mogą spowodować, że inny wątek mógł dodać lub usunąć klucz między dwoma wywołaniami.
sgnsajgon

To powinna być zaznaczona odpowiedź na pytanie, mimo że post Lasse jest doskonały.
Spivonious

13

Czy widzieliście reaktywne rozszerzenia dla .Net 3.5sp1. Według Jona Skeeta, oni przenieśli pakiet równoległych rozszerzeń i współbieżnych struktur danych dla .Net3.5 sp1.

Istnieje zestaw próbek dla .Net 4 Beta 2, który dość dobrze opisuje, jak używać ich równoległych rozszerzeń.

Właśnie spędziłem ostatni tydzień na testowaniu ConcurrentDictionary przy użyciu 32 wątków do wykonywania operacji we / wy. Wygląda na to, że działa zgodnie z reklamą, co wskazywałoby, że włożono w to ogromną ilość testów.

Edycja : .NET 4 ConcurrentDictionary i Patterns.

Firma Microsoft wydała plik PDF o nazwie Patterns of Paralell Programming. Naprawdę warto go pobrać, ponieważ opisał w naprawdę fajnych szczegółach właściwe wzorce do użycia dla rozszerzeń .Net 4 Concurrent i wzorce anty, których należy unikać. Tutaj jest.


4

Zasadniczo chcesz korzystać z nowego ConcurrentDictionary. Od razu po wyjęciu z pudełka musisz napisać mniej kodu, aby programy były bezpieczne dla wątków.


Czy jest jakiś problem, jeśli zdecyduję się na Concurrent Dictionery?
kbvishnu

1

ConcurrentDictionaryJest świetnym rozwiązaniem, jeśli spełnia wszystkie Twoje potrzeby wątku bezpieczeństwa. Jeśli tak nie jest, innymi słowy, jeśli robisz coś nieco złożonego, normalny Dictionary+ lockmoże być lepszą opcją. Na przykład załóżmy, że dodajesz jakieś zamówienia do słownika i chcesz na bieżąco aktualizować ich łączną kwotę. Możesz napisać taki kod:

private ConcurrentDictionary<int, Order> _dict;
private Decimal _totalAmount = 0;

while (await enumerator.MoveNextAsync())
{
    Order order = enumerator.Current;
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

Ten kod nie jest bezpieczny wątkowo. Wiele wątków aktualizujących _totalAmountpole może pozostawić je w stanie uszkodzonym. Możesz więc spróbować go chronić za pomocą lock:

_dict.TryAdd(order.Id, order);
lock (_locker) _totalAmount += order.Amount;

Ten kod jest „bezpieczniejszy”, ale nadal nie jest bezpieczny dla wątków. Nie ma gwarancji, że _totalAmountjest on zgodny z hasłami w słowniku. Inny wątek może próbować odczytać te wartości, aby zaktualizować element interfejsu użytkownika:

Decimal totalAmount;
lock (_locker) totalAmount = _totalAmount;
UpdateUI(_dict.Count, totalAmount);

totalAmountNie może odpowiadać liczbie zleceń w słowniku. Wyświetlane statystyki mogą być błędne. W tym momencie zdasz sobie sprawę, że musisz rozszerzyć lockochronę o aktualizację słownika:

// Thread A
lock (_locker)
{
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

// Thread B
int ordersCount;
Decimal totalAmount;
lock (_locker)
{
    ordersCount = _dict.Count;
    totalAmount = _totalAmount;
}
UpdateUI(ordersCount, totalAmount);

Ten kod jest całkowicie bezpieczny, ale wszystkie korzyści z używania go ConcurrentDictionaryzniknęły.

  1. Wydajność stała się gorsza niż w przypadku zwykłego Dictionary, ponieważ wewnętrzne blokowanie ConcurrentDictionaryjest teraz marnotrawne i zbędne.
  2. Musisz przejrzeć cały kod pod kątem niezabezpieczonych zastosowań udostępnionych zmiennych.
  3. Utknąłeś z używaniem niezręcznego API ( TryAdd?, AddOrUpdate?).

Tak więc moja rada brzmi: zacznij od Dictionary+ locki zachowaj opcję późniejszej aktualizacji do a ConcurrentDictionaryjako optymalizacji wydajności, jeśli ta opcja jest faktycznie wykonalna. W wielu przypadkach tak się nie stanie.


0

Użyliśmy ConcurrentDictionary do zbuforowanej kolekcji, która jest ponownie wypełniana co 1 godzinę, a następnie odczytywana przez wiele wątków klienta, podobnie jak rozwiązanie dla Czy ten przykładowy wątek jest bezpieczny? pytanie.

Okazało się, że zmiana go na  ReadOnlyDictionary  poprawiła ogólną wydajność.


ReadOnlyDictionary jest po prostu opakowaniem wokół innego IDictionary. Czego używasz pod maską?
Lu55

@ Lu55, korzystaliśmy z biblioteki MS .Net i wybieraliśmy spośród dostępnych / odpowiednich słowników. Nie wiem o wewnętrznej implementacji ReadOnlyDictionary MS
Michael Freidgeim
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.