Parallel.ForEach vs Task.Run e Task.WhenAll


158

Quais são as diferenças entre usar Parallel.ForEach ou Task.Run () para iniciar um conjunto de tarefas de forma assíncrona?

Versão 1:

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

Versão 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
Eu acho que o segundo fragmento de código seria quase igual ao primeiro se você usasse em Task.WaitAllvez de Task.WhenAll.
Avo1

15
Observe também que o segundo executará DoSomething ("s3") três vezes e não produzirá o mesmo resultado! stackoverflow.com/questions/4684320/…
Nullius 14/11


@ Dan: note que a versão 2 usa assíncrono / espera, o que significa que é uma pergunta diferente. O Async / waitit foi introduzido no VS 2012, 1,5 anos após a gravação do possível encadeamento duplicado.
precisa

Respostas:


159

Nesse caso, o segundo método aguardará assincronamente a conclusão das tarefas, em vez de o bloquear.

No entanto, há uma desvantagem a ser usada Task.Runem um loop - Com Parallel.ForEach, existe uma Partitionerque é criada para evitar a realização de mais tarefas do que o necessário. Task.Runsempre criará uma única tarefa por item (já que você está fazendo isso), mas os Parallellotes da classe funcionam para que você crie menos tarefas que o total de itens de trabalho. Isso pode proporcionar um desempenho geral significativamente melhor, especialmente se o corpo do loop tiver uma pequena quantidade de trabalho por item.

Se for esse o caso, você pode combinar as duas opções escrevendo:

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

Observe que isso também pode ser escrito neste formato mais curto:

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

1
Ótima resposta, eu queria saber se você poderia me indicar um bom material de leitura sobre este tópico?
Dimitar Dimitrov

@DimitarDimitrov Para coisas geral TPL, reedcopsey.com/series/parallelism-in-net4
Reed Copsey

1
Minha construção Parallel.ForEach estava travando meu aplicativo. Eu estava realizando algum processamento pesado de imagem dentro dele. No entanto, quando adicionei Task.Run (() => Parallel.ForEach (....)); Parou de bater. Você pode explicar o porquê? Observe que eu restrinja as opções paralelas ao número de núcleos no sistema.
monkeyjumps

3
E se DoSomethingfor async void DoSomething?
Francesco Bonizzi

1
Que tal async Task DoSomething?
precisa

37

A primeira versão bloqueará de forma síncrona o segmento de chamada (e executará algumas das tarefas nele).
Se for um thread da interface do usuário, isso congelará a interface do usuário.

A segunda versão executará as tarefas de forma assíncrona no pool de threads e liberará o thread de chamada até que elas sejam concluídas.

Também existem diferenças nos algoritmos de agendamento utilizados.

Observe que seu segundo exemplo pode ser reduzido para

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

2
não deveria ser await Task.WhenAll(strings.Select(async s => await Task.Run(() => DoSomething(s)));? Eu tive problemas ao retornar tarefas (em vez de esperar), especialmente quando declarações como usingestavam envolvidas para descartar objetos.
Martín Coll

Minha chamada Parallel.ForEach estava causando o travamento da interface do usuário. Adicionei Task.Run (() => Parallel.ForEach (....)); para ele e resolveu bater.
monkeyjumps

1

Acabei fazendo isso, pois parecia mais fácil ler:

  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);

Dessa maneira, as tarefas estão sendo executadas uma após a outra ou o WhenAll está iniciando todas de uma vez?
Vinicius Gualberto

Até onde eu sei, tudo começou quando eu chamo "DoSomethingAsync ()". Nada os está bloqueando até que o WhenAll seja chamado.
25418 Chris M.

Você quer dizer quando o primeiro "DoSomethingAsync ()" é chamado?
Vinicius Gualberto

1
@Crisma. Ele será bloqueado até a primeira espera de DoSomethingAsync (), pois é isso que transferirá a execução de volta ao seu loop. Se é síncrona e seu retorno de uma tarefa, todo o código será executado um após o outro eo WhenAll irá esperar por todas as tarefas a serem concluídas
Simon Belanger

0

Eu vi Parallel.ForEach usado de forma inadequada e achei que um exemplo nesta pergunta ajudaria.

Ao executar o código abaixo em um aplicativo de console, você verá como as tarefas executadas no Parallel.ForEach não bloqueiam o segmento de chamada. Isso pode ser bom se você não se importar com o resultado (positivo ou negativo), mas se precisar do resultado, certifique-se de usar o 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}]");
        }
    }
}

Aqui está o resultado:

insira a descrição da imagem aqui

Conclusão:

Usar o Parallel.ForEach com uma tarefa não bloqueará o thread de chamada. Se você se importa com o resultado, aguarde as tarefas.

~ Felicidades

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.