Parallel.ForEach vs Task.Factory.StartNew

267

Qual é a diferença entre os trechos de código abaixo? Os dois não usarão threads de pool de threads?

Por exemplo, se eu quiser chamar uma função para cada item de uma coleção,

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

vs

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

Respostas:

302

A primeira é uma opção muito melhor.

Parallel.ForEach, internamente, usa a Partitioner<T>para distribuir sua coleção em itens de trabalho. Ele não executará uma tarefa por item, mas fará o lote para reduzir a sobrecarga envolvida.

A segunda opção agendará um único Taskpor item em sua coleção. Embora os resultados sejam (quase) os mesmos, isso apresentará muito mais sobrecarga do que o necessário, especialmente para coleções grandes, e fará com que os tempos de execução gerais sejam mais lentos.

FYI - O Partitioner usado pode ser controlado usando as sobrecargas apropriadas para Parallel.ForEach , se desejado. Para detalhes, consulte Particionadores personalizados no MSDN.

A principal diferença, em tempo de execução, é a segunda que será assíncrona. Isso pode ser duplicado usando Parallel.ForEach, fazendo o seguinte:

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

Ao fazer isso, você ainda aproveita os particionadores, mas não bloqueia até que a operação esteja concluída.

Reed Copsey
fonte
8
IIRC, o particionamento padrão feito pelo Parallel.ForEach também leva em consideração o número de threads de hardware disponíveis, evitando que você precise calcular o número ideal de tarefas para iniciar. Confira o artigo Patterns of Parallel Programming da Microsoft ; tem ótimas explicações de todas essas coisas nele.
Mal Ross
2
@ Mal: ​​Mais ou menos ... Na verdade, esse não é o Partitioner, mas o trabalho do TaskScheduler. O TaskScheduler, por padrão, usa o novo ThreadPool, que lida com isso muito bem agora.
Reed Copsey
Obrigado. Eu sabia que deveria ter deixado a advertência "não sou especialista, mas ...". :)
Mal Ross
@ReedCopsey: Como anexar tarefas iniciadas via Parallel.ForEach à tarefa do wrapper? Para que, quando você chama .Wait () em uma tarefa do wrapper, ele seja interrompido até que as tarefas em execução paralela sejam concluídas?
Konstantin Tarkus
1
@Tarkus Se você estiver fazendo várias solicitações, é melhor usar HttpClient.GetString em cada item de trabalho (no loop Parallel). Não há razão para pôr dentro opção async do loop já concorrente, normalmente ...
Reed Copsey
89

Eu fiz um pequeno experimento de execução do método "1.000.000.000 (um bilhão)" de vezes com "Parallel.For" e um com objetos "Task".

Eu medi o tempo do processador e achei o Parallel mais eficiente. Parallel.For divide sua tarefa em pequenos itens de trabalho e os executa em todos os núcleos paralelamente da maneira ideal. Ao criar muitos objetos de tarefa (o FYI TPL utilizará o pool de threads internamente), cada execução será executada em cada tarefa, criando mais estresse na caixa, o que é evidente no experimento abaixo.

Também criei um pequeno vídeo que explica o TPL básico e também demonstrou como o Parallel.For utiliza seu núcleo com mais eficiência http://www.youtube.com/watch?v=No7QqSc5cl8 em comparação com tarefas e threads normais.

Experiência 1

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

Experiência 2

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

Comparação de tempo do processador

Shivprasad Koirala
fonte
Seria mais eficiente e a razão por trás da criação de threads é cara A Experiência 2 é uma prática muito ruim.
Tim
@ Georgi, por favor, se preocupe em falar mais sobre o que é ruim.
Shivprasad Koirala
3
Sinto muito, meu erro, eu deveria ter esclarecido. Quero dizer a criação de tarefas em um loop para 1000000000. A sobrecarga é inimaginável. Sem mencionar que o Parallel não pode criar mais de 63 tarefas por vez, o que o torna muito mais otimizado no caso.
você
Isso é verdade para 1000000000 tarefas. No entanto, quando eu processo uma imagem (repetidamente, ampliando o fractal) e faço Parallel.For em linhas, muitos núcleos ficam ociosos enquanto aguarda a conclusão dos últimos threads. Para torná-lo mais rápido, subdividi os dados em 64 pacotes de trabalho e criei tarefas para ele. (Então, Task.WaitAll aguardará a conclusão.) A idéia é fazer com que os threads ociosos selecionem um pacote de trabalho para ajudar a concluir o trabalho, em vez de esperar que 1-2 threads concluam seu bloco (Parallel.For) designado.
Tedd Hansen
1
O que Mehthod1()faz neste exemplo?
Zapnologica
17

O Parallel.ForEach otimizará (pode nem mesmo iniciar novos encadeamentos) e bloqueará até que o loop seja concluído, e o Task.Factory criará explicitamente uma nova instância de tarefa para cada item e retornará antes que eles sejam concluídos (tarefas assíncronas). Parallel.Foreach é muito mais eficiente.

Sogger
fonte
11

Na minha opinião, o cenário mais realista é quando as tarefas têm uma operação pesada para concluir. A abordagem de Shivprasad se concentra mais na criação de objetos / alocação de memória do que na própria computação. Fiz uma pesquisa chamando o seguinte método:

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

A execução deste método leva cerca de 0,5 segundo.

Liguei 200 vezes usando o Parallel:

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

Então eu liguei 200 vezes usando o modo antiquado:

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

Primeiro caso concluído em 26656ms, o segundo em 24478ms. Eu repeti isso muitas vezes. Sempre que a segunda abordagem é marginalmente mais rápida.

user1089583
fonte
Usar Parallel.For é a maneira antiquada. O uso da tarefa é recomendado para unidades de trabalho que não são uniformes. Os MVPs da Microsoft e os designers do TPL também mencionam que o uso do Tasks usará os threads com mais eficiência, e não bloqueie tantos enquanto aguarda a conclusão de outras unidades.
Suncat2000 28/01