2013-02-18 12 views
7

Podczas gdy odpowiedź na this question jest doskonała, oznacza to, że należy otoczyć wywołania List.ToArray() w zamku dla współbieżności. this blog post oznacza również, że może zawieść katastrofalnie (ale rzadko). Zwykle używam ToArray zamiast blokowania podczas wyliczania list lub innych kolekcji w celu uniknięcia wyjątku "Modified Collection, Enumeration may complete". Ta odpowiedź i post na blogu zakwestionowały to założenie.Czy ToArray() może wyrzucić wyjątek?

Dokumentacja dla List.ToArray() nie zawiera żadnych wyjątków, więc zawsze zakładałem, że zawsze będzie ona kompletna (choć może z nieaktualnymi danymi) i że nie jest bezpieczna dla wątków z punktu widzenia spójności danych , jest bezpieczny dla wątków z punktu widzenia wykonania kodu - innymi słowy, nie wyrzuci wyjątku i wywołanie go nie uszkodzi wewnętrznej struktury danych podstawowej kolekcji.

Jeśli to założenie nie jest poprawne, to mimo że nigdy nie spowodowało problemu, może to być timebomb w aplikacji wysokiej dostępności. Jaka jest ostateczna odpowiedź?

+0

Lista . ADD nie rzuca również wyjątku, jeśli inne wątki modyfikują listę w tym samym czasie. Nadal nie jest bezpieczny dla wątków. Po prostu sprawdza, czy nie modyfikujesz i nie wyliczasz go w tym samym czasie w tym samym wątku. Co sprawia, że ​​uważasz, że metoda, która nie została udokumentowana jako bezpieczna dla wątków, może być bezpieczna dla wątków? (Zakładając, że mówisz listy .ToArray lub Enumerable.ToArray. ConcurrentBag .ToArray jest thread-safe, jak wylicza się ConcurrentBag bez ToArray.) – dtb

+0

I zawężony zakres pytania skupić się na listy i listy ponieważ to one są dla mnie najbardziej niepokojące. Widziałem tę technikę stosowaną w wielu popularnych frameworkach open source, więc nie sądzę, że jestem jedynym, który zakłada założenie o "bezpieczeństwie" tej techniki. Nie pytam, czy są bezpieczne dla wątków (z definicji nie są), ale czy powinienem rozpocząć wyszukiwanie i naprawianie wystąpień "niebezpiecznych" wywołań ToArray()? –

+0

Czy jesteś pewien, że te frameworki open source używają ToArray zamiast blokady, czy też używają ToArray do modyfikowania listy podczas wyliczania jej w tym samym wątku? – dtb

Odpowiedz

5

Nie można znaleźć dokumentacji dotyczącej możliwych wyjątków metody ToArray z jednego prostego powodu. Jest to metoda rozszerzenia, która ma wiele "przeciążeń". Wszystkie mają ten sam podpis metody, ale implementacja jest różna dla różnych typów kolekcji, np. List<T> i HashSet<T>.

Możemy jednak założyć bezpieczne założenie dla większości kodu, że struktura .NET BCL nie wykonuje żadnego blokowania ze względu na wydajność. Sprawdziłem również bardzo konkretnie implementację ToList dla List<T>.

public T[] ToArray() 
{ 
    T[] array = new T[this._size]; 
    Array.Copy(this._items, 0, array, 0, this._size); 
    return array; 
} 

Jak można sobie wyobrazić, jest dość prosty kod, który kończy się wykonaniem w mscorlib. W przypadku tej konkretnej implementacji można również wyświetlić wyjątki, które mogą wystąpić in MSDN page for Array.Copy method. Sprowadza się on do wyjątku, który jest generowany, gdy pozycja listy zmienia się zaraz po przydzieleniu tablicy docelowej.

Mając na uwadze, że List<T> jest trywialnym przykładem, można sobie wyobrazić, że szanse na wzrost wyjątku na strukturach, które wymagają bardziej skomplikowanego kodu, aby przechowywać w tablicy. Implementacja Queue<T> jest kandydatem, który ma większe szanse na niepowodzenie:

public T[] ToArray() 
{ 
    T[] array = new T[this._size]; 
    if (this._size == 0) 
    { 
     return array; 
    } 
    if (this._head < this._tail) 
    { 
     Array.Copy(this._array, this._head, array, 0, this._size); 
    } 
    else 
    { 
     Array.Copy(this._array, this._head, array, 0, this._array.Length - this._head); 
     Array.Copy(this._array, 0, array, this._array.Length - this._head, this._tail); 
    } 
    return array; 
} 
+0

Świetna analiza. To odpowiedź na pytanie ostatecznie. Dzięki! –

0

Przede wszystkim musisz wyjaśnić, że callsite musi znajdować się w regionie, w którym wątki są bezpieczne. Większość regionów w twoim kodzie nie będzie regionami z bezpiecznymi wątkami i przejmie jeden wątek wykonania w danym momencie (dla większości kodów aplikacji). Dla (bardzo przybliżonego oszacowania) 99% wszystkich kodów aplikacji to pytanie nie ma sensu.

Po drugie, musisz wyjaśnić, "czym" jest funkcja wyliczeniowa, ponieważ będzie ona zależeć od typu wyliczenia, przez który przechodzisz - czy mówisz o normalnym rozszerzeniu linq do wyliczeń?

Po trzecie, łącze, które podajesz do kodu ToArray i oświadczenie blokujące dookoła niego, jest w najlepszym razie bzdurą: Bez pokazywania, że ​​callsite blokuje się w tej samej kolekcji, nie gwarantuje to bezpieczeństwa nici w al.

I tak dalej.

+2

Poprosiłabym o awans, ale ostatnie zdanie naprawdę nie było konieczne. Nie wszyscy są całkowicie przeciwni teorii bezpieczeństwa i nie można oczekiwać, że tak. –

+0

Poprawiam się, podobnie jak mój tekst. Poparłem cię za udzielenie mi dobrej lekcji;) –

4

Gdy bezpieczeństwo wątków nie jest wyraźnie zagwarantowane przez dokumenty lub zasady, nie można tego przejąć. Jeśli założysz to, ryzykujesz wprowadzenie do produkcji klasy błędów, które są niewiarygodne i mogą potencjalnie kosztować dużo wydajności/dostępności/pieniędzy. Czy chcesz podjąć to ryzyko?

Nigdy nie można przetestować czegoś, co będzie bezpieczne dla wątków. Nigdy nie można być pewnym. Nie można mieć pewności, że przyszła wersja zachowuje się w ten sam sposób.

Zrób to we właściwy sposób i zablokuj.

Przy okazji te uwagi dotyczyły List.ToArray, która jest jedną z bezpieczniejszych wersji ToArray. Rozumiem, dlaczego błędnie sądzono, że można go używać jednocześnie z zapisami do listy. Oczywiście IEnumerable.ToArraynie może być threadssafe, ponieważ jest to właściwość sekwencji podstawowej.

+0

Wartość -1 jest, ponieważ odpowiedź jest zbyt rozwlekła i zbyt ogólna. Czasami właściwą rzeczą jest blokowanie, ale czasem właściwym wyjściem jest wybranie wątku lub równoległej struktury/algorytmu danych. Dodatkowo, są * testy *, które możesz wykonać na kodzie, który ma być bezpieczny dla wątków, aby go zweryfikować. –

+0

@ 280Z28 Szanuję twoją krytykę. Odpowiedź miała być dość ogólna, ponieważ podstawowy problem jest taki sam dla całej klasy pytań. Jestem po prostu podejrzany o twoje ostatnie stwierdzenie: jakie testy wykonasz, aby upewnić się, że ToArray jest w 100% bezpieczny? Coś mniej jest wysyłką bomby do produkcji. Czy kiedykolwiek wykonywałeś operacje? Nienawidzę * otrzymywania 10 wiadomości o błędach dziennie od niedeterministycznego błędu. – usr

+0

Jestem właściwie pewien, że dobrze zrozumiałeś sytuację, ale konkretna prezentacja może okazać się myląca dla innych programistów, którzy nie są tak doświadczeni. Twoje oświadczenie o testowaniu nie jest ograniczone do testowania 'ToArray', ale zamiast tego sprawia wrażenie, że" testowanie jako bezpieczne dla wątków "nie jest ogólnie możliwe. –

0

Wydaje się mylą dwie rzeczy:

  • Lista <T> nie obsługuje modyfikowane, gdy jest wyliczone. Podczas wyliczania listy moduł wyliczający sprawdza, czy lista została zmodyfikowana po każdej iteracji. Lista telefoniczna <T> .ToArray przed wyliczeniem listy rozwiązuje ten problem, ponieważ wyliczasz migawkę listy, a nie samą listę.

  • Lista <T> nie jest kolekcją wątków. Wszystkie powyższe zakłada dostęp z tego samego wątku. Dostęp do listy z dwóch wątków zawsze wymaga blokady. Lista <T>. ToArray nie jest wątkiem bezpiecznym i nie pomaga tutaj.

2

ToArray NIE jest tematyczny, a ten kod to potwierdza!

Rozważmy następujący kod raczej śmieszny:

 List<int> l = new List<int>(); 

     for (int i = 1; i < 100; i++) 
     { 
      l.Add(i); 
      l.Add(i * 2); 
      l.Add(i * i); 
     } 

     Thread th = new Thread(new ThreadStart(() => 
     { 
      int t=0; 
      while (true) 
      { 
       //Thread.Sleep(200); 

       switch (t) 
       { 
        case 0: 
         l.Add(t); 
         t = 1; 
         break; 
        case 1: 
         l.RemoveAt(t); 
         t = 0; 
         break; 
       } 
      } 
     })); 

     th.Start(); 

     try 
     { 
      while (true) 
      { 
       Array ai = l.ToArray(); 

       //foreach (object o in ai) 
       //{ 
       // String str = o.ToString(); 
       //} 
      } 
     } 
     catch (System.Exception ex) 
     { 
      String str = ex.ToString();     
     } 

    } 

Kod ten nie powiedzie się w bardzo krótkim czasie, ze względu na linii l.Add(t). Ponieważ ToArray NIE jest wątkowo bezpieczny, przydzieli tablicę do bieżącego rozmiaru l, wtedy dodamy element do l (w drugim wątku), a następnie spróbuje skopiować bieżący rozmiar l do ai i nie powiedzie się ponieważ mam zbyt wiele elementów. ToArray wyrzuca ArgumentException.

Powiązane problemy