Parallel.ForEach vs Task.Run i Task.WhenAll


158

Jakie są różnice między używaniem Parallel.ForEach lub Task.Run () do asynchronicznego uruchamiania zestawu zadań?

Wersja 1:

List<string> strings = new List<string> { "s1", "s2", "s3" };
Parallel.ForEach(strings, s =>
{
    DoSomething(s);
});

Wersja 2:

List<string> strings = new List<string> { "s1", "s2", "s3" };
List<Task> Tasks = new List<Task>();
foreach (var s in strings)
{
    Tasks.Add(Task.Run(() => DoSomething(s)));
}
await Task.WhenAll(Tasks);

3
Myślę, że drugi fragment kodu byłby prawie równy pierwszemu, gdybyś użył go Task.WaitAllzamiast Task.WhenAll.
avo

15
Należy również pamiętać, że druga z nich wykona DoSomething ("s3") trzy razy i nie da tego samego wyniku! stackoverflow.com/questions/4684320/…
Nullius


@Dan: zwróć uwagę, że wersja 2 używa async / await, co oznacza, że ​​to inne pytanie. Async / await został wprowadzony w VS 2012, 1,5 roku po napisaniu możliwego zduplikowanego wątku.
Petter T

Odpowiedzi:


159

W takim przypadku druga metoda będzie asynchronicznie czekać na zakończenie zadań zamiast blokowania.

Jednak użycie Task.Runpętli ma tę wadę Parallel.ForEach, że jest Partitionertworzona, aby uniknąć wykonywania większej liczby zadań niż to konieczne. Task.Runzawsze utworzy jedno zadanie na element (ponieważ to robisz), ale Parallelpartie klas działają, więc utworzysz mniej zadań niż wszystkie elementy pracy. Może to zapewnić znacznie lepszą ogólną wydajność, zwłaszcza jeśli treść pętli wymaga niewielkiej ilości pracy na element.

W takim przypadku możesz połączyć obie opcje, pisząc:

await Task.Run(() => Parallel.ForEach(strings, s =>
{
    DoSomething(s);
}));

Zauważ, że można to również zapisać w tej krótszej formie:

await Task.Run(() => Parallel.ForEach(strings, DoSomething));

1
Świetna odpowiedź, zastanawiałem się, czy możesz wskazać mi dobry materiał do czytania na ten temat?
Dimitar Dimitrov

@DimitarDimitrov Informacje ogólne na temat TPL: reedcopsey.com/series/parallelism-in-net4
Reed Copsey

1
Moja konstrukcja Parallel.ForEach powodowała awarię mojej aplikacji. Wykonywałem w nim ciężkie przetwarzanie obrazu. Jednak kiedy dodałem Task.Run (() => Parallel.ForEach (....)); Przestał się zawieszać. Czy możesz mi wytłumaczyć dlaczego? Należy pamiętać, że ograniczam opcje równoległe do liczby rdzeni w systemie.
małpki skacze

3
A jeśli DoSomethingtak async void DoSomething?
Francesco Bonizzi

1
O co chodzi async Task DoSomething?
Shawn Mclean

37

Pierwsza wersja będzie synchronicznie blokować wątek wywołujący (i uruchamiać na nim niektóre zadania).
Jeśli jest to wątek interfejsu użytkownika, spowoduje to zawieszenie interfejsu użytkownika.

Druga wersja będzie uruchamiać zadania asynchronicznie w puli wątków i zwalniać wątek wywołujący, dopóki nie zostaną wykonane.

Istnieją również różnice w stosowanych algorytmach planowania.

Zwróć uwagę, że drugi przykład można skrócić do

await Task.WhenAll(strings.Select(s => Task.Run(() => DoSomething(s)));

2
nie powinno być await Task.WhenAll(strings.Select(async s => await Task.Run(() => DoSomething(s)));? Miałem problemy podczas zwracania zadań (zamiast czekania), zwłaszcza gdy instrukcje takie jak usingbyły używane do usuwania obiektów.
Martín Coll

Moje wywołanie Parallel.ForEach powodowało awarię interfejsu użytkownika Dodałem Task.Run (() => Parallel.ForEach (....)); do niego i rozwiązał awarię.
małpie skoki

1

Skończyło się na tym, że to było łatwiejsze do odczytania:

  List<Task> x = new List<Task>();
  foreach(var s in myCollectionOfObject)
  {
      // Note there is no await here. Just collection the Tasks
      x.Add(s.DoSomethingAsync());
  }
  await Task.WhenAll(x);

W ten sposób robisz Zadania są wykonywane jeden po drugim lub WhenAll uruchamia je wszystkie naraz?
Vinicius Gualberto

O ile wiem, wszystkie są uruchamiane, gdy wywołuję „DoSomethingAsync ()”. Jednak nic ich nie blokuje, dopóki nie zostanie wywołana funkcja WhenAll.
Chris M.

Masz na myśli, kiedy wywoływana jest pierwsza funkcja „DoSomethingAsync ()”?
Vinicius Gualberto

1
@Krzyżmo. Zostanie zablokowany do pierwszego oczekiwania na DoSomethingAsync (), ponieważ to właśnie przeniesie wykonanie z powrotem do pętli. Jeśli jest synchroniczny i zwrócisz zadanie, cały kod zostanie uruchomiony jeden po drugim, a WhenAll będzie czekać na zakończenie wszystkich zadań
Simon Belanger,

0

Widziałem użycie Parallel.ForEach niewłaściwie i pomyślałem, że przykład w tym pytaniu może pomóc.

Po uruchomieniu poniższego kodu w aplikacji konsoli zobaczysz, jak zadania wykonywane w Parallel.ForEach nie blokują wątku wywołującego. Może to być w porządku, jeśli nie zależy Ci na wyniku (dodatnim lub ujemnym), ale jeśli potrzebujesz wyniku, powinieneś upewnić się, że używasz Task.WhenAll.

using System;
using System.Linq;
using System.Threading.Tasks;

namespace ParrellelEachExample
{
    class Program
    {
        static void Main(string[] args)
        {
            var indexes = new int[] { 1, 2, 3 };

            RunExample((prefix) => Parallel.ForEach(indexes, (i) => DoSomethingAsync(i, prefix)),
                "Parallel.Foreach");

            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine("*You'll notice the tasks haven't run yet, because the main thread was not blocked*");
            Console.WriteLine("Press any key to start the next example...");
            Console.ReadKey();

            RunExample((prefix) => Task.WhenAll(indexes.Select(i => DoSomethingAsync(i, prefix)).ToArray()).Wait(),
                "Task.WhenAll");
            Console.WriteLine("All tasks are done.  Press any key to close...");
            Console.ReadKey();
        }

        static void RunExample(Action<string> action, string prefix)
        {
            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine($"{Environment.NewLine}Starting '{prefix}'...");
            action(prefix);
            Console.WriteLine($"{Environment.NewLine}Finished '{prefix}'{Environment.NewLine}");
        }


        static async Task DoSomethingAsync(int i, string prefix)
        {
            await Task.Delay(i * 1000);
            Console.WriteLine($"Finished: {prefix}[{i}]");
        }
    }
}

Oto wynik:

wprowadź opis obrazu tutaj

Wniosek:

Użycie Parallel.ForEach z Task nie spowoduje zablokowania wątku wywołującego. Jeśli zależy Ci na wyniku, czekaj na zadania.

~ Pozdrawiam

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.