Równolegle.ForEach może powodować wyjątek "Out of Memory", jeśli pracuje z wyliczanym z dużym obiektem

Próbuję przenieść bazę danych, w której obrazy były przechowywane w bazie danych do rekordu w bazie danych wskazującego na plik na dysku twardym. Próbowałem użyć Parallel.ForEach, Aby przyspieszyć proces używając tej metody, aby uzyskać zapytania od danych.

Jednak zauważyłem, że dostaję OutOfMemory wyjątek. Wiem, że Parallel.ForEach odpytuje partię enumerables, aby zmniejszyć koszt narzutu, jeśli istnieje jeden do odstępowania zapytań (więc twoje źródło będzie bardziej prawdopodobne, że następny rekord buforowane w pamięci, jeśli wykonasz kilka zapytań naraz zamiast odstępować je od siebie). Problem jest spowodowany jednym z rekordów, które zwracam, jest tablica bajtów 1-4MB, która powoduje buforowanie całej przestrzeni adresowej (program musi działać w trybie x86, ponieważ docelowa platforma będzie maszyną 32-bitową)

Czy Jest jakiś sposób, aby wyłączyć buforowanie lub make jest mniejszy dla TPL?


Oto przykładowy program pokazujący problem. To musi być skompilowane w trybie x86 do Pokaż problem, jeśli trwa długo lub nie dzieje się na twoim komputerze, zwiększając rozmiar tablicy (znalazłem 1 << 20 trwa około 30 sekund na moim komputerze i 4 << 20 był prawie natychmiastowy)

class Program
{

    static void Main(string[] args)
    {
        Parallel.ForEach(CreateData(), (data) =>
            {
                data[0] = 1;
            });
    }

    static IEnumerable<byte[]> CreateData()
    {
        while (true)
        {
            yield return new byte[1 << 20]; //1Mb array
        }
    }
}
Author: iPython, 2011-08-08

3 answers

Domyślne opcje dla Parallel.ForEach działa dobrze tylko wtedy, gdy zadanie jest związane z procesorem i skaluje się liniowo . Gdy zadanie jest związane z procesorem, wszystko działa idealnie. Jeśli masz czterordzeniowy procesor i nie działają żadne inne procesy, Parallel.ForEach używa wszystkich czterech procesorów. Jeśli masz czterordzeniowy procesor i jakiś inny proces na twoim komputerze używa jednego pełnego procesora, Parallel.ForEach używa mniej więcej trzech procesorów.

Ale jeśli zadanie nie jest związane z procesorem, to Parallel.ForEach kontynuuje uruchamianie zadań, starając się zachować wszystkie Procesory zajęte. Jednak bez względu na to, ile zadań działa równolegle, zawsze jest więcej nieużywanej mocy procesora, a więc wciąż tworzy zadania.

Skąd wiesz, czy Twoje zadanie jest związane z procesorem? Mam nadzieję, że tylko przez Inspekcję. Jeśli faktorujesz liczby pierwsze, jest to oczywiste. Ale inne przypadki nie są tak oczywiste. Empirycznym sposobem, aby stwierdzić, czy Twoje zadanie jest związane z procesorem, jest ograniczenie maksymalnego stopnia równoległości z ParallelOptions.MaximumDegreeOfParallelism i obserwuj, jak zachowuje się twój program. Jeśli Twoje zadanie jest Procesor związany to powinieneś zobaczyć taki wzór na czterordzeniowym systemie:

  • ParallelOptions.MaximumDegreeOfParallelism = 1: użyj jednego pełnego PROCESORA lub 25% wykorzystania procesora
  • ParallelOptions.MaximumDegreeOfParallelism = 2: użyj dwóch procesorów lub 50% wykorzystania procesora
  • ParallelOptions.MaximumDegreeOfParallelism = 4: użyj wszystkich procesorów lub 100% wykorzystania procesora

Jeśli zachowuje się tak, możesz użyć domyślnych opcji Parallel.ForEach i uzyskać dobre wyniki. Liniowe wykorzystanie procesora oznacza dobre planowanie zadań.

Ale jeśli uruchomię Twoją przykładową aplikację na moim Intel i7, dostanę około 20% Wykorzystanie procesora bez względu na to, jaki maksymalny stopień równoległości ustawiłem. Dlaczego? Tak dużo pamięci jest przydzielane, że garbage collector blokuje wątki. Aplikacja jest związana z zasobami, a zasobem jest pamięć.

Podobnie zadanie związane z I / O, które wykonuje długie zapytania przeciwko serwerowi bazy danych, również nigdy nie będzie w stanie efektywnie wykorzystać wszystkich zasobów procesora dostępnych na komputerze lokalnym. I w takich przypadkach Harmonogram zadań nie jest w stanie " wiedzieć, kiedy to stop " rozpoczynanie nowych zadań.

Jeśli Twoje zadanie nie jest związane z procesorem lub wykorzystanie procesora nie skaluje się liniowo z maksymalnym stopniem równoległości, powinieneś doradzić Parallel.ForEach, aby nie uruchamiać zbyt wielu zadań na raz. Najprostszym sposobem jest określenie liczby, która pozwala na pewną równoległość dla nakładających się zadań związanych z We/Wy, ale nie tak bardzo, że przytłaczasz zapotrzebowanie lokalnego komputera na zasoby lub przewyższasz wszelkie zdalne serwery. Próby i błędy są zaangażowane, aby uzyskać najlepsze wyniki:

static void Main(string[] args)
{
    Parallel.ForEach(CreateData(),
        new ParallelOptions { MaxDegreeOfParallelism = 4 },
        (data) =>
            {
                data[0] = 1;
            });
}
 88
Author: Rick Sladkey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-08-08 04:08:56

Tak więc, podczas gdy to, co zasugerował Rick jest zdecydowanie ważnym punktem, inną rzeczą, której myślę, że brakuje, jest dyskusja partycjonowanie.

Parallel::ForEach użyje domyślnego Partitioner<T> implementacja, która dla IEnumerable<T>, która nie ma znanej długości, użyje strategii partycjonowania fragmentów. Oznacza to, że każdy wątek roboczy, który Parallel::ForEach będzie używany do pracy na zbiorze danych, odczyta pewną liczbę elementów z IEnumerable<T>, które będą następnie przetwarzane tylko przez ten wątek (na razie ignorując kradzież pracy). Robi to, aby zaoszczędzić na kosztach ciągłego powrotu do źródła i przydzielenia nowej pracy i zaplanowania jej dla innego wątku roboczego. Zazwyczaj jest to dobra rzecz.Jednak w twoim konkretnym scenariuszu wyobraź sobie, że jesteś na czterordzeniowym rdzeniu i ustawiłeś MaxDegreeOfParallelism do 4 wątków do twojej pracy, a teraz każdy z nich wyciąga kawałek 100 elementów z twojej IEnumerable<T>. To jest 100-400 megs właśnie tam, tylko dla tego konkretnego wątku roboczego, prawda?

Więc jak to rozwiązać? Proste, Ty napisz niestandardową Partitioner<T> implementację. Teraz chunking jest nadal przydatny w Twoim przypadku, więc prawdopodobnie nie chcesz iść z jednym elementem strategii partycjonowania, ponieważ wtedy wprowadziłbyś overhead z całą koordynacją zadań niezbędną do tego. Zamiast tego napiszę konfigurowalną wersję, którą można dostroić za pomocą aplikacji, aż znajdziesz optymalną równowagę dla swojego obciążenia. Dobrą wiadomością jest to, że pisząc takie implementacja jest dość prosta, nie musisz nawet pisać jej samemu, ponieważ zespół PFX już to zrobił i umieścił ją w projekcie parallel programming samples .

 40
Author: Drew Marsh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-09-03 15:36:46

Ten problem ma wszystko wspólnego z partycjonerami, a nie ze stopniem równoległości. Rozwiązaniem jest wdrożenie niestandardowego partycjonera danych.

Jeśli zbiór danych jest duży, wydaje się, że mono implementacja TPL jest gwarantowana zabrakło pamięci.Zdarzyło mi się to niedawno (w zasadzie uruchamiałem powyższą pętlę i stwierdziłem, że pamięć wzrosła liniowo, dopóki nie dała mi wyjątku OOM).

Po wyśledzeniu problemu stwierdziłem, że domyślnie mono będzie dzieliło up the enumerator używając klasy EnumerablePartitioner. Ta klasa ma zachowanie w tym, że za każdym razem, gdy przekazuje dane do zadania, " kawałki" danych o stale rosnącym (i niezmiennym) współczynniku 2. Więc pierwszy gdy zadanie zapyta o dane, otrzymuje fragment o rozmiarze 1, następnym razem o rozmiarze 2*1=2, następnym razem 2*2=4, Następnie 2 * 4=8, itd. itd. Wynik jest taki, że ilość danych przekazywanych do zadania, a zatem przechowywanych w pamięci jednocześnie zwiększa się wraz z długością zadania, a jeśli ilość danych jest przetwarzany, nieuchronnie występuje wyjątek z pamięci.

Przypuszczalnie pierwotnym powodem takiego zachowania jest to, że chce uniknąć każdy wątek zwraca wiele razy, aby uzyskać dane, ale wydaje się, że w oparciu o założenie, że wszystkie przetwarzane dane mogą zmieścić się w pamięci (Nie dotyczy to odczytu z dużych plików).

Tego problemu można uniknąć za pomocą niestandardowego partycjonera, jak wspomniano wcześniej. Jeden ogólny przykład takiego, który po prostu zwraca dane do każdego zadania po jednym elemencie na raz są tutaj:

Https://gist.github.com/evolvedmicrobe/7997971

Najpierw Utwórz instancję tej klasy i przekaż ją do Parallel.For zamiast samego wyliczenia

 12
Author: evolvedmicrobe,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-12-17 00:45:46