Parallel.ForEach vs Task.Factory.StartNew


267

Jaka jest różnica między poniższymi fragmentami kodu? Czy oba nie będą używać wątków puli wątków?

Na przykład, jeśli chcę wywołać funkcję dla każdego elementu w kolekcji,

Parallel.ForEach<Item>(items, item => DoSomething(item));

vs

foreach(var item in items)
{
  Task.Factory.StartNew(() => DoSomething(item));
}

Odpowiedzi:


302

Pierwsza jest znacznie lepszą opcją.

Parallel.ForEach, wewnętrznie, używa Partitioner<T>do dystrybucji twojej kolekcji na elementy pracy. Nie wykona jednego zadania na przedmiot, a raczej zgrupuje go, aby obniżyć związane z tym koszty ogólne.

Druga opcja zaplanuje jeden Taskna element w Twojej kolekcji. Chociaż wyniki będą (prawie) takie same, spowoduje to znacznie więcej kosztów ogólnych, niż to konieczne, szczególnie w przypadku dużych kolekcji, i spowoduje, że ogólne czasy działania będą wolniejsze.

FYI - używanym Partycjonerem można sterować za pomocą odpowiednich przeciążeń dla Parallel.ForEach , jeśli jest to pożądane. Aby uzyskać szczegółowe informacje, zobacz Niestandardowe partycjonery w MSDN.

Główną różnicą w czasie wykonywania jest to, że druga będzie działać asynchronicznie. Można to powielić za pomocą Parallel.ForEach, wykonując:

Task.Factory.StartNew( () => Parallel.ForEach<Item>(items, item => DoSomething(item)));

W ten sposób nadal korzystasz z partycjonerów, ale nie blokuj, dopóki operacja nie zostanie zakończona.


8
IIRC, domyślny system partycjonowania wykonywany przez Parallel.ForEach, bierze również pod uwagę liczbę dostępnych wątków sprzętowych, dzięki czemu nie musisz ustalać optymalnej liczby zadań do uruchomienia. Przeczytaj artykuł Microsoft's Patterns of Parallel Programming ; zawiera świetne wyjaśnienia wszystkich tych rzeczy.
Mal Ross

2
@Mal: Coś w rodzaju ... To właściwie nie jest partycjoner, ale raczej zadanie TaskScheduler. TaskScheduler domyślnie używa nowego ThreadPool, który teraz bardzo dobrze sobie z tym radzi.
Reed Copsey

Dzięki. Wiedziałem, że powinienem był odejść w zastrzeżeniu „Nie jestem ekspertem, ale ...”. :)
Mal Ross

@ReedCopsey: Jak dołączyć zadania rozpoczęte przez Parallel.ForEach do zadania opakowania? Tak więc, gdy wywołujesz funkcję .Wait () dla zadania opakowania, zawiesza się, aż zadania uruchomione równolegle zostaną zakończone?
Konstantin Tarkus

1
@Tarkus Jeśli wysyłasz wiele żądań, lepiej użyć HttpClient.GetString w każdym elemencie pracy (w pętli równoległej). Nie ma powodu, aby umieszczać opcję asynchroniczną w już współbieżnej pętli, zwykle ...
Reed Copsey

89

Zrobiłem mały eksperyment z użyciem metody „1 000 000 000 (jeden miliard)” razy z „Parallel.For” i jednym z obiektami „Task”.

Zmierzyłem czas procesora i stwierdziłem, że Parallel jest bardziej wydajny. Równolegle. Dzieli twoje zadanie na małe elementy robocze i wykonuje je równolegle na wszystkich rdzeniach w optymalny sposób. Podczas tworzenia wielu obiektów zadań (FYI TPL wewnętrznie będzie korzystała z pulowania wątków) będzie przenosić każde wykonanie każdego zadania, powodując więcej stresu w polu, co jest widoczne z poniższego eksperymentu.

Stworzyłem również małe wideo, które wyjaśnia podstawową licencję TPL, a także pokazuje, jak Parallel.For bardziej efektywnie wykorzystuje Twój rdzeń http://www.youtube.com/watch?v=No7QqSc5cl8 w porównaniu do normalnych zadań i wątków.

Eksperyment 1

Parallel.For(0, 1000000000, x => Method1());

Eksperyment 2

for (int i = 0; i < 1000000000; i++)
{
    Task o = new Task(Method1);
    o.Start();
}

Porównanie czasu procesora


Byłoby to bardziej wydajne, a powodem, dla którego tworzenie wątków jest kosztowne. Eksperyment 2 jest bardzo złą praktyką.
Tim

@ Georgi-proszę o rozmowę o tym, co jest złe.
Shivprasad Koirala

3
Przepraszam, mój błąd, powinienem był wyjaśnić. Mam na myśli tworzenie zadań w pętli do 1000000000. Narzut jest niewyobrażalny. Nie wspominając o tym, że Parallel nie może utworzyć więcej niż 63 zadań na raz, co czyni go znacznie bardziej zoptymalizowanym w tym przypadku.
Georgi-it

Dotyczy to 1000000000 zadań. Jednak gdy przetwarzam obraz (kilkakrotnie, powiększając fraktal) i robię równolegle, na liniach wiele rdzeni jest bezczynnych, czekając na zakończenie ostatnich wątków. Aby przyspieszyć, sam podzieliłem dane na 64 pakiety robocze i stworzyłem dla nich zadania. (Następnie Task.WaitAll czekać na zakończenie.) Chodzi o to, aby bezczynne wątki podniosły pakiet roboczy, aby pomóc zakończyć pracę, zamiast czekać na 1-2 wątki, aby ukończyć przypisaną porcję (Parallel.For).
Tedd Hansen

1
Co robi Mehthod1()w tym przykładzie?
Zapnologica

17

Parallel.ForEach zoptymalizuje (może nawet nie rozpocząć nowych wątków) i zablokuje, dopóki pętla nie zostanie zakończona, a Task.Factory wyraźnie utworzy nową instancję zadania dla każdego elementu i zwróci przed zakończeniem (zadania asynchroniczne). Parallel.Foreach jest znacznie wydajniejszy.


11

Moim zdaniem najbardziej realistycznym scenariuszem jest sytuacja, w której zadania wymagają dużej operacji. Podejście Shivprasad koncentruje się bardziej na tworzeniu obiektów / alokacji pamięci niż na samym przetwarzaniu. Przeprowadziłem badanie, które wywołało następującą metodę:

public static double SumRootN(int root)
{
    double result = 0;
    for (int i = 1; i < 10000000; i++)
        {
            result += Math.Exp(Math.Log(i) / root);
        }
        return result; 
}

Wykonanie tej metody zajmuje około 0,5 sekundy.

Nazwałem to 200 razy przy użyciu Parallel:

Parallel.For(0, 200, (int i) =>
{
    SumRootN(10);
});

Potem nazwałem to 200 razy staromodnym sposobem:

List<Task> tasks = new List<Task>() ;
for (int i = 0; i < loopCounter; i++)
{
    Task t = new Task(() => SumRootN(10));
    t.Start();
    tasks.Add(t);
}

Task.WaitAll(tasks.ToArray()); 

Pierwszy przypadek ukończono w 26656 ms, drugi w 24478 ms. Powtórzyłem to wiele razy. Za każdym razem drugie podejście jest marginalnie szybsze.


Korzystanie z Parallel.For to staromodny sposób. Korzystanie z zadania jest zalecane w przypadku jednostek pracy, które nie są jednolite. Microsoft MVP i projektanci TPL również wspominają, że korzystanie z Zadań pozwoli na efektywniejsze wykorzystanie wątków, ienot blokuje ich tyle, czekając na ukończenie innych jednostek.
Suncat2000
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.