2013-02-20 6 views
9

Niektórzy programiści twierdzą, że lepiej jest przekazywać parametry IEnumerable<T> przez przekazywanie implementacji takich jak List<T>, a jednym z popularnych powodów jest to, że interfejs API jest od razu dostępny w większej liczbie scenariuszy, ponieważ więcej zbiorów będzie kompatybilnych z IEnumerable<T> niż jakakolwiek inna konkretna implementacja, np List<T>.Czy T [] nie jest lepszy niż IEnumerable <T> jako typ parametru? (Biorąc pod uwagę wyzwania związane z nawiercaniem)

Im więcej nurkuję w wielowątkowych scenariuszach rozwoju, tym bardziej zaczynam myśleć, że IEnumerable<T> nie jest właściwym typem, a postaram się wyjaśnić, dlaczego poniżej.

Czy otrzymałeś następujący wyjątek podczas wyliczania kolekcji?

Kolekcja została zmodyfikowana; operacja wyliczania może nie zostać wykonana. (InvalidOperationException)

Collection was modified exception

Zasadniczo co powoduje to, dostaniesz kolekcję na jednym wątku, podczas gdy ktoś inny modyfikuje kolekcji na innym wątku.

Aby obejść ten problem, że następnie wywołano zwyczaj zrobić zdjęcie gromadzenia przed wymienić go przez przekształcenie kolekcję tablicy wewnątrz metody, na przykład:

static void LookAtCollection(IEnumerable<int> collection) 
{ 
    foreach (int Value in collection.ToArray() /* take a snapshot by converting to an array */) 
    { 
     Thread.Sleep(ITEM_DELAY_MS); 
    } 
} 

pytanie jest to. Czy nie lepiej jest kodować w kierunku tablic zamiast wyliczeń jako ogólną zasadę, nawet jeśli oznacza to, że osoby dzwoniące są teraz zmuszone do konwersji swoich kolekcji na tablice przy korzystaniu z interfejsu API?

Czy to nie jest czystsze i bardziej kuloodporne? (Typ parametr jest tablicą zamiast)

static void LookAtCollection(int[] collection) 
{ 
    foreach (int Value in collection) 
    { 
     Thread.Sleep(ITEM_DELAY_MS); 
    } 
} 

Tablica spełnia wszystkie wymagania jest przeliczalny i stałej długości, a rozmówca jest świadom faktu, że będzie działać na migawce kolekcji w tym momencie, co może zaoszczędzić więcej błędów.

Jedyną lepszą alternatywą, którą mogę teraz znaleźć, jest IReadOnlyCollection, która będzie jeszcze bardziej odporna na pociski, ponieważ kolekcja jest również dostępna tylko w trybie "zawartość-przedmiot".

EDIT:

@DanielPryden warunkiem link do bardzo ładnej artykule "Arrays considered somewhat harmful". I komentarze pisarza "Rzadko potrzebuję kolekcji, która ma dość sprzeczne właściwości bycia całkowicie zmiennym, a jednocześnie utrwalonym rozmiarem" i "W prawie każdym przypadku istnieje lepsze narzędzie do używaj niż tablicy. " rodzaj przekonania mnie, że tablice nie są tak blisko srebrnej kuli, jak się spodziewałem, i zgadzam się z ryzykiem i lukami. Myślę, że teraz interfejs IReadOnlyCollection<T> jest lepszą alternatywą niż zarówno tablica, jak i IEnumerable<T>, ale w pewnym sensie pozostawia mi to pytanie teraz: Czy wywoływacz wymusza na nim użycie parametru o typie IReadOnlyCollection<T> w deklaracji metody? A może dzwoniący nadal może decydować, która implementacja IEnumerable<T> przejdzie do metody, która patrzy na kolekcję?

Dzięki za wszystkie odpowiedzi. Wiele nauczyłem się z tych reakcji.

+7

Nie, ta sama łódź. Możesz wkręcić z tablicą _elements_ w drugim wątku. Nawet kod, który wysłałeś z '.ToArray' nie jest bezpieczny dla wątków, jeśli kolekcja zmienia się podczas iteracji, aby przekonwertować ją do tablicy. Najlepszym rozwiązaniem jest udokumentowanie (podpis metody, dokumenty itp.) Faktu, że wybierasz metodę gwintowania i pozostawiasz odpowiedzialność za dzwoniącego, aby zapewnić ci kolekcję, z którą można pracować w sposób gwintowany. Pozwól mu zrobić kopie (lub zapewnij bezpieczną kolekcję). Jeśli dadzą ci śmieci, dajesz im błędy "InvalidOperationException". –

+2

[Lock vs. ToArray for thread safe dostęp do listy kolekcji List] (http://stackoverflow.com/questions/3128889/lock-vs-toarray-for-thread-safe-foreach-access-of-list-collection) –

+1

Zapomniałem wspomnieć, że możesz tworzyć sygnatury swoich metod, żądać jakiegoś zbioru wątków lub innego mechanizmu blokującego, ale to oznacza, że ​​będziesz musiał narzucić ten mechanizm swoim rozmówcom. EDYCJA: Możesz także wykonać kopię _ przed rozpoczęciem pracy w dodatkowe wątki; to kopiowanie byłoby synchroniczne z kodem wywołującym. Jedyną możliwą przyczyną problemów z wątkami będzie to, że wywołujący wykonuje również operacje gwintowania w tym samym zbiorze (ja jestem z łodzi, która utrzymuje ją w prostocie i po prostu pozostawia odpowiedzialność rozmówcy, ale to zależy od zamierzonego API/usage) –

Odpowiedz

4

Prawdziwy „problemem” jest to, że podczas T[] jest prawdopodobnie bardziej specyficzny typ parametru niż byłby idealny i pozwala odbiorcy bezpłatny dostęp do napisania jakiegokolwiek elementu (które mogą lub nie mogą być pożądane), IEnumerable<T> jest zbyt ogólny . Z punktu widzenia bezpieczeństwa typowego, wywołujący może podać odniesienie do dowolnego obiektu, który implementuje IEnumerable<T>, ale w rzeczywistości tylko niektóre z takich obiektów będą działać i nie ma żadnego prostego sposobu, aby dzwoniący wiedział, które to są.

Jeśli T jest rodzajem referencyjną, T[] będzie z natury bezpieczny wątku do czytania, pisania i liczenia, z zastrzeżeniem spełnienia określonych warunków (np wątki korzystające różne elementy nie będą ingerować w ogóle, jeśli jeden wątek zapisuje pozycję wokół gdy inny wątek odczyta lub wylicza, ostatni wątek zobaczy stare lub nowe dane, żaden z interfejsów kolekcji firmy Microsoft nie oferuje żadnych takich gwarancji, ani nie zapewnia żadnych środków, za pomocą których kolekcja może wskazać, jakie gwarancje może lub nie może uzyskać.

Moja skłonność polegałaby na użyciu T[] lub zdefiniowaniu IThreadSafeList<T> where T:class, która zawierałaby takie elementy, jak CompareExchangeItem, które Pozwolić na Interlocked.CompareExchange na element, ale nie obejmuje rzeczy takich jak Insert i Remove, których nie można zrobić w sposób bezpieczny dla wątków.

+0

[Potrzebne cytowanie]! Napisanie nowego odwołania do zmiennej typu referencyjnego jest 32-bitową operacją atomową, ale nie jestem pewien, czy zachowanie jest wymagane przez specyfikację. Ale nawet jeśli tak było, samo zaktualizowanie referencji atomowej nie jest wystarczające: aby bezpiecznie opublikować obiekt w innym wątku, potrzebujesz również bariery pamięci. –

+0

@DanielPryden: Specyfikacja bardzo wyraźnie określa, że ​​zapisy odniesienia nie będą "dzielone". Jeśli chodzi o bariery pamięciowe, semantyka jest taka, że ​​lokalizacja może być zapisana w dowolnym czasie, pomiędzy żądaniem zapisu, a następującą barierą pamięci, a wartość widziana przez oprogramowanie może być dowolna między poprzednią barierą odczytu a tym, kiedy ma miejsce żądanie odczytu. ; w wielu przypadkach taka semantyka może być akceptowalna. Jeśli kontrolka pokazuje paczkę pasków postępu dla różnych operacji, które są w toku, to dobrze, jeśli paski postępu nie reprezentują 100% aktualnych informacji, ale ... – supercat

+0

... aktualizacja pasków stanu nie powinny utrudniać postępu wątkom, które wykonują prawdziwą pracę, ani nie należy próbować zgłaszać postępów różnych zadań w wyniku prac. Jeśli zadanie zostanie dodane podczas wyliczania, aktualizacja statusu będzie całkowicie szczęśliwa, jeśli nowe zadanie zostanie uwzględnione, lub jeśli nie jest; podobnie, jeśli praca kończy się i zostaje usunięta z kolekcji. Jeśli widok stanu ma barierę pamięci przed rozpoczęciem wyliczania, nie będzie się przejmował, jeśli zobaczy zmiany, które wystąpią podczas wyliczania. – supercat

2

Ten problem nie dotyczył nawlekania. Spróbuj zmodyfikować kolekcję wewnątrz pętli, która jawnie lub niejawnie wywołuje GetEnumerator. Wynik jest taki sam. To nielegalna operacja.

+0

Prawda. To stwarza ten sam problem, nawet w tym samym wątku. Też mi się to przytrafiło i wymaga starannego planowania, jak to naprawić. –

1

Wątki nigdy nie powinny mieć dostępu do tych samych obiektów, co inne wątki. Powinieneś zawsze duplikować zmienne/obiekty/tablice i analizować je w czasie wolnym wątków.

Po zakończeniu należy oddzwonić do głównego wątku, aby wątek mógł zaktualizować bieżące wartości/połączyć wartości.

Jeśli dwa obiekty mogą dosięgnąć tego samego obiektu w tym samym czasie, co się dostaje, lub przeciąganie liny, wyjątki lub dziwne zachowanie.

Wyobraź sobie takie wątki: Masz szefa i dwóch pracowników. Szef robi kopie listu, który ma w ręce i przekazuje go pracownikom. Każdy pracownik ma swoje własne zadania, jeden do zebrania wszystkiego z listy, drugi do sprawdzenia, czy pozycje na liście należy zamówić u dostawcy. Każdy z nich idzie do swoich prywatnych pokoi, a szef kładzie papier na biurku, gdzie czasami sprawdza, czy ktoś wie, ile jest w tej chwili.

Pracownik jeden zwraca zamówione przedmioty i przekazuje je dyrektorowi, który przekazuje je do działu logistyki. Godzina później pracownik drugi powraca z zaktualizowaną listą z ilością zapasów. Reżyser wyrzuca starą listę i zastępuje ją nową listą.

Teraz scenariusz, w którym znajduje się jedna lista.

Szef podaje te same instrukcje. Pracownik pierwszy bierze listę i zaczyna dzwonić do pierwszego dostawcy. Pracownik 2 bierze listę i zaczyna zbierać przedmioty. Reżyser chce spojrzeć na listę, ale lista już nie istnieje, a cały lejek sprzedaży zatrzymuje się, ponieważ nie ma dokładnych informacji, ponieważ lista jest zajęta. Pierwszy pracownik zakończył dzwonienie i chce zaktualizować swoją listę. Nie może, ponieważ lista jest zajęta i nie może kontynuować następnego przedmiotu, ponieważ lista zniknęła, więc utknął w kciuki lub rzuca kuksańcem. Kiedy pracownik drugi jest gotowy ze swoją listą, pracownik bierze listę i próbuje zakończyć wywoływanie statusów zamówień, ale dyrektor burza się i bierze listę i przekazuje informacje do działu sprzedaży. Dyrektor i pracownik rozpoczynają walkę o listę, co powoduje, że żadna praca nie jest wykonywana, a firma umiera.

Dlatego zawsze musisz pracować z niezależnymi kopiami w wątkach, chyba że wiesz tylko, że wątek wymaga wyłącznie dostępu do tego zasobu, ponieważ nigdy nie wiesz, kiedy ten wątek będzie robił coś z tym zasobem, ponieważ nie wiesz, t kontroluj taktowanie procesora.

Mam nadzieję, że to trochę pomaga.

+0

Rozumiem twoją analogię, ale nie sądzę, że można uczciwie powiedzieć, że nici nigdy nie powinny mieć dostępu do tych samych obiektów. To by znacznie zmniejszyło przydatność wątków. –

+0

To jest cały cel wątków. Są niezależnymi programami. Musisz nadać wątkom określone misje do wykonania, a następnie złożyć raport po ich zakończeniu. Nie powinny dokonywać krytycznych modyfikacji obiektów, których potrzebuje inny kod programu. Dobrym pomysłem na gwintowanie będzie obliczanie kropli tła dla gry, rejestrowania, intensywnych obliczeń, sprawdzania aktualizacji itp. Po uruchomieniu rundy mogą zgłosić, że ukończyli i przekazać wyniki do głównego wątku, który robi z nim swoją magię. http://en.wikipedia.org/wiki/Deadlock – Tschallacka

+0

Ale proste blokowanie lub inne rodzaje uzgadniania mogą z łatwością umożliwić pracę wielu wątków na tych samych danych, zwłaszcza jeśli jest to po prostu dostęp do odczytu. –

0

Jedną z idei stojących za IEnumerable jest to, że jest wynikiem zapytania LINQ. Ponieważ zapytanie jest zamiast deklaracji zamiast wykonanego kodu, wynik IEnumerable jest wywoływany tylko podczas iterowania w foreach lub kopiowania elementów do Array lub innej struktury. Teraz, jeśli chcemy zapisać wynik w statycznej strukturze, takiej jak Array, będziemy musieli zaktualizować go przed iteracją, a także IEnumerable lub otrzymamy nieaktualne dane.IEnumerable.ToArray oznacza: Zaktualizuj kolekcję i utwórz jej statyczną kopię:. Tak więc moja odpowiedź brzmi, że jeśli musimy zrobić aktualizację przed operacją na kolekcji, to IEnumerable jest bardziej zwięzły. W innych scenariuszach możemy użyć Array w celu uproszczenia kodu.

+0

Interfejs 'IEnumerable' /' IEnumerator' poprzedza 'LINQ' przez wiele lat. Podstawową ideą jest to, że umożliwiają kodowi nadanie innym kodom sekwencji pojedynczych elementów, bez konieczności posiadania przez kod odbiorcy informacji o sposobie wyprowadzania elementów; niestety, interfejsy nie dostarczają * żadnych * informacji, nawet rzeczy, które mogłyby być przydatne (np.czy istnieje jakakolwiek okoliczność, w której zestaw przedmiotów wyliczony przez kolekcję może się zmienić, lub czy zbiór z natury jest "znany", czy też może szybko dowiedzieć się, ile jest w nim elementów. – supercat

+0

@supercat: Masz rację. Biorąc pod uwagę "IEnumerable" wiemy tylko, że możemy dokonać iteracji poprzez kolekcję i nic więcej. Implementator jest nawet w stanie dostarczyć 'IEnumerable' nieskończonego zestawu losowych elementów (np.' While (true) yield return random.Next() '). Jeśli więc pracujemy z podejrzanym i nieznanym źródłem, nie możemy nawet założyć, że 'IEnumerable' jest skończony lub niezmienny. Rozważam tutaj "IEnountable" jako iterator, z którego chcemy uzyskać aktualne dane. W takim przypadku użyłbym 'IEnumerable' zamiast' Array'. –

4

Większość metod nie jest wątkowo bezpieczna, chyba że wyraźnie udokumentowano inaczej.

W tym przypadku powiedziałbym, że to zależy od osoby dzwoniącej, która zadba o bezpieczeństwo nici. Jeśli wywołujący przekazuje do metody parametr IEnumerable<T>, nie powinien się dziwić, jeśli metoda go wyliczy! Jeśli wyliczenie nie jest bezpieczne dla wątków, wywołujący musi utworzyć migawkę w sposób bezpieczny dla wątków i przekazać migawkę.

wywoływany nie może tego zrobić, dzwoniąc ToArray() lub ToList() - metody te będą również wyliczyć kolekcję, a wywoływany nie może wiedzieć, co jest wymagane (np Lock), aby migawkę w sposób bezpieczny wątku .

+0

Podoba mi się twoja odpowiedź, ale nie sądzisz, że jeśli masz dużo InvalidOperationExceptions w tej samej metodzie, a każdy z nich jest przypadkiem, w którym inny wątek modyfikuje kolekcję, to jest to problem do rozwiązania, a nie każdy z nich. wina dzwoniącego? Czy nie byłoby lepiej, gdyby metoda wzięła tablicę i uniknęła również przyszłych błędów? I zastosować tę samą zasadę do innych metod związanych ze zbieraniem? –

+2

@MartinLottering - nie, nie zgadzam się, że byłoby lepiej. Wielowątkowy rozmówca zawsze musi wziąć pod uwagę bezpieczeństwo wątku i wybrać najbardziej odpowiednie rozwiązanie dla swojego scenariusza. Może to być zrobienie migawki kolekcji (ToArray), ale w niektórych przypadkach może to być inne rozwiązanie (np. Trzymanie blokady podczas wykonywania tej metody). – Joe

+0

OK, ale zgadzam się z tobą, że dzwoniący powinien utworzyć tablicę. Po prostu myślę, że kanclerz powinien również to wymusić, wymagając 'T []' zamiast 'IEnumerable '. –

Powiązane problemy