Byłem zachwycony, widząc nową System.Collections.Concurrent
przestrzeń nazw w .Net 4.0, całkiem fajnie! Widziałem ConcurrentDictionary
, ConcurrentQueue
, ConcurrentStack
, ConcurrentBag
i BlockingCollection
.
Jedną z rzeczy, której wydaje się być tajemniczo brakuje, jest ConcurrentList<T>
. Czy sam muszę to napisać (lub pobrać z Internetu :))?
Czy brakuje mi czegoś oczywistego?
Odpowiedzi:
I spróbowaliśmy jakiś czas temu (też: na GitHub ). Moja implementacja miała pewne problemy, o których nie będę tutaj wchodził. Co ważniejsze, powiem ci, czego się nauczyłem.
Po pierwsze, nie ma możliwości, aby uzyskać pełną implementację
IList<T>
tego bez blokady i wątków. W szczególności losowe wstawianie i usuwanie nie będzie działać, chyba że zapomnisz także o losowym dostępie O (1) (tj. Chyba że „oszukasz” i po prostu użyjesz jakiejś połączonej listy i pozwolisz indeksacji ssać).Co ja pomyślałem może być wart był bezpieczny wątku, ograniczony podzbiór
IList<T>
: w szczególności taki, który pozwala osobieAdd
i zapewnić losową tylko do odczytu dostępu indeksu (ale nieInsert
,RemoveAt
itp, a także nie przypadkowy zapisu dostępu).To był cel mojej
ConcurrentList<T>
realizacji . Ale kiedy przetestowałem jego wydajność w scenariuszach wielowątkowych, stwierdziłem, że zwykła synchronizacja dodaje do aList<T>
była szybsza . Zasadniczo dodawanie doList<T>
jest już błyskawiczne; złożoność zaangażowanych kroków obliczeniowych jest niewielka (zwiększ indeks i przypisz elementowi tablicy; to naprawdę tyle ). Byś potrzebował mnóstwo jednoczesnych zapisów zobaczyć jakiejkolwiek blokady niezgody na ten temat; i nawet wtedy średnia wydajność każdego zapisu nadal przewyższałaby droższą, choć bez blokady, implementacjęConcurrentList<T>
.W stosunkowo rzadkim przypadku, gdy wewnętrzna tablica listy musi się zmienić, płacisz niewielki koszt. W końcu doszedłem do wniosku, że był to jedyny niszowy scenariusz, w którym
ConcurrentList<T>
typ kolekcji tylko do dodania miałby sens: gdy chcesz zagwarantować niski koszt dodawania elementu przy każdym pojedynczym wywołaniu (w przeciwieństwie do zamortyzowanego celu wydajności).To po prostu nie jest tak przydatna klasa, jak mogłoby się wydawać.
źródło
List<T>
tego, co używa synchronizacji opartej na starym skoolu, monitor jestSynchronizedCollection<T>
ukryty w BCL: msdn.microsoft.com/en-us/library/ms668265.aspxConcurrentList
wygrana byłaby wygrana, miałaby miejsce, gdy lista nie byłaby zbyt aktywna, ale jest wielu współbieżnych czytelników. Można zmniejszyć obciążenie czytelników do pojedynczej bariery pamięci (i nawet to wyeliminować, jeśli czytelnicy nie przejmują się nieco przestarzałymi danymi).ConcurrentList<T>
takiego w taki sposób, że czytelnicy mają zagwarantowany spójny stan bez potrzeby blokowania, jest stosunkowo proste. Gdy lista rozszerzy się z np. Rozmiaru 32 na 64, zachowaj tablicę rozmiaru 32 i utwórz nową tablicę rozmiaru 64. Podczas dodawania każdego z następnych 32 elementów włóż go do gniazda 32-63 nowej tablicy i skopiuj stary element z tablicy o rozmiarze 32 do nowej. Dopóki nie zostanie dodany 64. element, czytelnicy będą szukać w tablicy rozmiar 32 dla pozycji 0-31 oraz w tablicy rozmiar 64 dla pozycji 32-63.Do czego użyłbyś ConcurrentList?
Koncepcja kontenera o dostępie swobodnym w świecie wątków nie jest tak przydatna, jak może się wydawać. Twierdzenie
jako całość nadal nie byłaby bezpieczna dla wątków.
Zamiast tworzyć ConcurrentList, spróbuj budować rozwiązania z tym, co tam jest. Najpopularniejsze klasy to ConcurrentBag, a zwłaszcza BlockingCollection.
źródło
Monitor
, nie ma powodu, aby istniała lista współbieżna.Z całym szacunkiem dla już udzielonych wspaniałych odpowiedzi, czasami po prostu chcę bezpiecznej dla wątków IList. Nic zaawansowanego ani wymyślnego. Wydajność jest ważna w wielu przypadkach, ale czasami nie stanowi to problemu. Tak, zawsze będą wyzwania bez metod takich jak „TryGetValue” itp., Ale w większości przypadków chcę tylko czegoś, co mogę wymienić bez konieczności martwienia się o blokowanie wszystkiego. I tak, ktoś prawdopodobnie może znaleźć „błąd” w mojej implementacji, który może doprowadzić do impasu lub czegoś (przypuszczam), ale bądźmy szczerzy: jeśli chodzi o wielowątkowość, jeśli nie napiszesz poprawnie kodu, to i tak jest w impasie. Mając to na uwadze, postanowiłem wykonać prostą implementację ConcurrentList, która zaspokoi te podstawowe potrzeby.
I za to, co warto: wykonałem podstawowy test dodania 10 000 000 pozycji do zwykłej listy i listy współbieżnych, a wyniki były następujące:
Lista zakończona za: 7793 milisekundy. Współbieżne ukończone za: 8064 milisekund.
źródło
RemoveAt(int index)
nigdy nieInsert(int index, T item)
jest bezpieczna dla wątków, jest bezpieczna tylko dla indeksu == 0, zwrotIndexOf()
jest natychmiast przestarzały itp. Nawet nie zaczynaj othis[int]
.ReaderWriterLockSlim
może być wykonane do impasu łatwo stosującEnterUpgradeableReadLock()
jednocześnie. Nie używasz go jednak, nie udostępniasz zamka na zewnątrz i nie wywołujesz np. Metody, która wchodzi w blokadę zapisu, przytrzymując blokadę odczytu, więc korzystanie z klasy nie powoduje już zakleszczenia prawdopodobne.var l = new ConcurrentList<string>(); /* ... */ l[0] += "asdf";
. Ogólnie rzecz biorąc, każda kombinacja odczytu i zapisu może prowadzić do poważnych kłopotów, gdy jest wykonywana jednocześnie. Dlatego współbieżnych struktur danych na ogół zapewniają metody, takie jak teConcurrentDictionary
„sAddOrGet
itp NB stałej (i zbędne, ponieważ członkowie są już oznaczone jako takie przez podkreślenie) powtarzaniethis.
zaśmiecanie.ConcurrentList
(jako tablica o zmiennym rozmiarze, a nie połączona lista) nie jest łatwo pisać przy użyciu operacji nieblokujących. Jego interfejs API nie przekłada się dobrze na wersję „współbieżną”.źródło
Powodem, dla którego nie ma ConcurrentList, jest to, że zasadniczo nie można go zapisać. Powodem jest to, że kilka ważnych operacji w IList opiera się na indeksach i po prostu nie działa. Na przykład:
Efektem, do którego dąży autor, jest wstawienie „pies” przed „kot”, ale w środowisku wielowątkowym wszystko może się zdarzyć na liście między tymi dwoma wierszami kodu. Na przykład mógłby zrobić inny wątek
list.RemoveAt(0)
, przesuwając całą listę w lewo, ale co najważniejsze, catIndex się nie zmieni. Wpływ na to jest taki, żeInsert
operacja spowoduje umieszczenie „psa” za kotem, a nie przed nim.Kilka implementacji, które widzisz jako „odpowiedzi” na to pytanie, ma dobre intencje, ale jak pokazuje powyższy, nie oferują wiarygodnych wyników. Jeśli naprawdę chcesz semantyki podobnej do listy w środowisku wielowątkowym, nie możesz się tam dostać, umieszczając blokady w metodach implementacji listy. Musisz upewnić się, że każdy indeks, którego używasz, działa całkowicie w kontekście blokady. Rezultatem jest to, że możesz używać Listy w środowisku wielowątkowym z właściwym blokowaniem, ale sama lista nie może istnieć w tym świecie.
Jeśli uważasz, że potrzebujesz współbieżnej listy, istnieją naprawdę tylko dwie możliwości:
Jeśli masz ConcurrentBag i jesteś w pozycji, w której musisz przekazać go jako IList, masz problem, ponieważ wywoływana metoda określa, że mogą spróbować zrobić coś takiego jak ja powyżej z cat & pies. W większości światów oznacza to, że metoda, którą wywołujesz, po prostu nie jest zbudowana do pracy w środowisku wielowątkowym. Oznacza to, że albo przebudujesz go tak, aby był, albo, jeśli nie możesz, będziesz musiał obchodzić się z nim bardzo ostrożnie. Prawie na pewno będziesz musiał stworzyć własną kolekcję z własnymi zamkami i wywołać metodę obrażającą w obrębie zamka.
źródło
W przypadkach, gdy odczyty znacznie przewyższają liczbę zapisów lub (jakkolwiek częste) zapisy nie są współbieżne , odpowiednie może być podejście kopiowania przy zapisie .
Implementacja pokazana poniżej to
var snap = _list; snap[snap.Count - 1];
nigdy (cóż, z wyjątkiem pustej listy oczywiście) nie rzuca, a także dostajesz bezpieczne wątkowo wyliczanie z semantyką migawek za darmo .. jak KOCHAM niezmienność!Aby kopiowanie przy zapisie działało, musisz utrzymywać swoje struktury danych w sposób niezmienny , tzn. Nikt nie może ich zmieniać po udostępnieniu ich innym wątkom. Kiedy chcesz zmodyfikować, ty
Kod
Stosowanie
Jeśli potrzebujesz większej wydajności, pomoże to uogólnić metodę, np. Utwórz jedną metodę dla każdego rodzaju modyfikacji (dodaj, usuń, ...), którą chcesz, i zapisz wskaźniki funkcji
cloner
iop
.Uwaga nr 1 Twoim obowiązkiem jest upewnić się, że nikt nie modyfikuje (rzekomo) niezmiennej struktury danych. Nic nie możemy zrobić w ogólnej implementacji, aby temu zapobiec, ale specjalizując się w niej
List<T>
, możesz uchronić się przed modyfikacją za pomocą List.AsReadOnly ()NB # 2 Uważaj na wartości z listy. Powyższe podejście do kopiowania chroni tylko ich członkostwo na liście, ale jeśli nie wstawisz ciągów, ale niektóre inne zmienne obiekty, musisz zadbać o bezpieczeństwo wątków (np. Blokowanie). Jest to jednak ortogonalne w stosunku do tego rozwiązania i np. Można zablokować zmienne wartości bez problemu. Musisz tylko być tego świadomy.
Uwaga nr 3 Jeśli struktura danych jest ogromna i często ją modyfikujesz, podejście kopiowania wszystkich danych w trakcie zapisu może być wygórowane zarówno pod względem zużycia pamięci, jak i kosztów procesora związanych z kopiowaniem. W takim przypadku możesz zamiast tego skorzystać z niezmiennych kolekcji MS .
źródło
System.Collections.Generic.List<t>
jest już bezpieczny dla wielu czytelników. Próba uczynienia tego wątkiem bezpiecznym dla wielu pisarzy nie miałaby sensu. (Z powodów, o których wspominali już Henk i Stephen)źródło
Niektórzy zauważyli niektóre punkty towarów (i niektóre z moich myśli):
To nie jest odpowiedź. To tylko komentarze, które tak naprawdę nie pasują do konkretnego miejsca.
... Mój wniosek, Microsoft musi wprowadzić głębokie zmiany w „foreach”, aby ułatwić korzystanie z kolekcji MultiThreaded. Musi również przestrzegać własnych zasad użytkowania IEnumerator. Do tego czasu możemy łatwo napisać MultiThreadList, który używałby iteratora blokującego, ale który nie byłby zgodny z „IList”. Zamiast tego będziesz musiał zdefiniować własny interfejs „IListPersonnal”, który może zawieść bez „wyjątku” przy „wstawianiu”, „usuwaniu” i przypadkowym akcesoriu (indeksatorze). Ale kto będzie chciał z niego korzystać, jeśli nie jest to standard?
źródło
ConcurrentOrderedBag<T>
która zawierałaby implementację tylko do odczytuIList<T>
, ale oferowałaby równieżint Add(T value)
metodę w pełni bezpieczną dla wątków . Nie rozumiem, dlaczegoForEach
potrzebne byłyby jakiekolwiek zmiany. Chociaż Microsoft nie mówi tego wprost, ich praktyka sugeruje, żeIEnumerator<T>
wyliczenie zawartości kolekcji, która istniała podczas jej tworzenia, jest całkowicie do przyjęcia ; wyjątek zmodyfikowany przez kolekcję jest wymagany tylko wtedy, gdy moduł wyliczający nie byłby w stanie zagwarantować bezproblemowego działania.GetEnumerator
metody pozostawiania kolekcji zablokowanej po jej zwróceniu; takie projekty mogą łatwo doprowadzić do impasu. NieIEnumerable<T>
zawiera wskazania, czy można oczekiwać zakończenia wyliczenia, nawet jeśli kolekcja zostanie zmodyfikowana; najlepsze, co można zrobić, to napisać własne metody, aby to zrobiły, i mieć metody, które akceptująIEnumerable<T>
dokumentowanie faktu, że będzie ono bezpieczne tylko pod warunkiem, żeIEnumerable<T>
obsługuje wyliczenie bezpieczne pod kątem wątków.IEnumerable<T>
włączenie metody „Snapshot” z typem zwracanymIEnumerable<T>
. Niezmienne kolekcje mogą się zwrócić; kolekcja ograniczona może, jeśli nic innego nie skopiować się doList<T>
lub,T[]
i wywołaćGetEnumerator
to. Niektóre niepowiązane kolekcje mogłyby zostać zaimplementowaneSnapshot
, a te, które nie byłyby w stanie zgłosić wyjątku bez próby wypełnienia listy swoją zawartością.W sekwencyjnie wykonywanym kodzie stosowane struktury danych różnią się od (dobrze napisanego) jednocześnie wykonującego się kodu. Powodem jest to, że kod sekwencyjny implikuje porządek niejawny. Współbieżny kod nie oznacza jednak żadnego zamówienia; jeszcze lepiej, implikuje to brak określonej kolejności!
Z tego powodu struktury danych z domyślną kolejnością (np. Lista) nie są zbyt przydatne do rozwiązywania równoczesnych problemów. Lista implikuje porządek, ale nie określa jasno, czym jest ta kolejność. Z tego powodu kolejność wykonywania kodu manipulującego listą określa (do pewnego stopnia) domyślną kolejność listy, która jest w bezpośrednim konflikcie z wydajnym rozwiązaniem współbieżnym.
Pamiętaj, że współbieżność to problem z danymi, a nie problem z kodem! Nie możesz najpierw zaimplementować kodu (lub przepisać istniejącego kodu sekwencyjnego) i uzyskać dobrze zaprojektowane współbieżne rozwiązanie. Najpierw musisz zaprojektować struktury danych, pamiętając o tym, że niejawne porządkowanie nie istnieje w systemie współbieżnym.
źródło
Bezblokowe kopiowanie i zapisywanie działa świetnie, jeśli nie masz do czynienia z zbyt dużą liczbą elementów. Oto klasa, którą napisałem:
przykładowe użycie: Orders_BUY = CopyAndWriteList.Clear (Orders_BUY);
źródło
Zaimplementowałem jeden podobny do Briana . Mój jest inny:
yield return
do tworzenia modułu wyliczającego.DoSync
orazGetSync
metody pozwalające na sekwencyjne interakcje wymagające wyłącznego dostępu do listy.Kod :
źródło
try
blokuRemove
lub ustawiacza indeksu w tym samym czasie?IList
semantyki w scenariuszach równoległych jest co najwyżej ograniczona. Napisałem ten kod prawdopodobnie zanim doszedłem do tej realizacji. Moje doświadczenie jest takie samo jak autor akceptowanej odpowiedzi: spróbowałem z tym, co wiedziałem o synchronizacji i IList <T> i nauczyłem się czegoś, robiąc to.