Parallel.ForEach vs Task.Factory.StartNew

267

Quelle est la différence entre les extraits de code ci-dessous? N'utilisera-t-il pas tous les deux des threads de pool de threads?

Par exemple, si je veux appeler une fonction pour chaque élément d'une collection,

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

vs

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

Réponses:

302

La première est une bien meilleure option.

Parallel.ForEach, en interne, utilise un Partitioner<T>pour distribuer votre collection en éléments de travail. Il ne fera pas une tâche par article, mais plutôt en lots pour réduire les frais généraux impliqués.

La deuxième option prévoit un seul Taskarticle par article dans votre collection. Bien que les résultats soient (presque) les mêmes, cela entraînera beaucoup plus de frais généraux que nécessaire, en particulier pour les grandes collections, et ralentira les temps d'exécution globaux.

FYI - Le partitionneur utilisé peut être contrôlé en utilisant les surcharges appropriées pour Parallel.ForEach , si vous le souhaitez. Pour plus de détails, voir Partitionneurs personnalisés sur MSDN.

La principale différence, lors de l'exécution, est que la seconde agira de manière asynchrone. Cela peut être dupliqué en utilisant Parallel.ForEach en faisant:

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

En faisant cela, vous profitez toujours des partitionneurs, mais ne bloquez pas tant que l'opération n'est pas terminée.

Reed Copsey
la source
8
IIRC, le partitionnement par défaut effectué par Parallel.ForEach prend également en compte le nombre de threads matériels disponibles, vous évitant d'avoir à déterminer le nombre optimal de tâches pour démarrer. Consultez l'article Patterns of Parallel Programming de Microsoft ; il contient d'excellentes explications sur tout cela.
Mal Ross
2
@Mal: En quelque sorte ... Ce n'est en fait pas le partitionneur, mais plutôt le travail du TaskScheduler. Le TaskScheduler, par défaut, utilise le nouveau ThreadPool, qui gère très bien cela maintenant.
Reed Copsey
Merci. Je savais que j'aurais dû partir dans la mise en garde "Je ne suis pas un expert, mais ...". :)
Mal Ross
@ReedCopsey: Comment attacher des tâches démarrées via Parallel.ForEach à la tâche wrapper? De sorte que lorsque vous appelez .Wait () sur une tâche wrapper, il se bloque jusqu'à ce que les tâches exécutées en parallèle soient terminées?
Konstantin Tarkus
1
@Tarkus Si vous faites plusieurs demandes, il vaut mieux utiliser simplement HttpClient.GetString dans chaque élément de travail (dans votre boucle parallèle). Aucune raison de mettre une option asynchrone à l'intérieur de la boucle déjà simultanée, généralement ...
Reed Copsey
89

J'ai fait une petite expérience de l'exécution d'une méthode "1 000 000 000 (un milliard)" fois avec "Parallel.For" et une avec des objets "Tâche".

J'ai mesuré le temps processeur et trouvé Parallel plus efficace. Parallel.For divise votre tâche en petits éléments de travail et les exécute sur tous les cœurs en parallèle de manière optimale. Lors de la création de nombreux objets de tâche (FYI TPL utilisera le pool de threads en interne), chaque exécution de chaque tâche se déplacera, ce qui créera plus de stress dans la boîte, comme le montre l'expérience ci-dessous.

J'ai également créé une petite vidéo qui explique le TPL de base et a également montré comment Parallel.For utilise votre noyau plus efficacement http://www.youtube.com/watch?v=No7QqSc5cl8 par rapport aux tâches et threads normaux.

Expérience 1

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

Expérience 2

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

Comparaison du temps processeur

Shivprasad Koirala
la source
Ce serait plus efficace et la raison pour laquelle la création de threads est coûteuse. L'expérience 2 est une très mauvaise pratique.
Tim
@ Georgi-it, veuillez vous soucier de parler davantage de ce qui est mauvais.
Shivprasad Koirala
3
Je suis désolé, mon erreur, j'aurais dû clarifier. Je veux dire la création de tâches dans une boucle à 1000000000. La surcharge est inimaginable. Sans oublier que le Parallel ne peut pas créer plus de 63 tâches à la fois, ce qui le rend beaucoup plus optimisé dans le cas.
Georgi-it
Cela est vrai pour 1000000000 tâches. Cependant, lorsque je traite une image (à plusieurs reprises, un zoom fractal) et que je fais Parallèle. Pour les lignes, beaucoup de cœurs sont inactifs en attendant que les derniers threads se terminent. Pour accélérer, j'ai moi-même subdivisé les données en 64 lots de travaux et créé des tâches pour cela. (Ensuite, Task.WaitAll attendra la fin.) L'idée est que les threads inactifs récupèrent un package de travail pour aider à terminer le travail au lieu d'attendre 1-2 threads pour terminer leur bloc (Parallel.For) attribué.
Tedd Hansen
1
Que fait Mehthod1()dans cet exemple?
Zapnologica
17

Parallel.ForEach optimisera (peut même ne pas démarrer de nouveaux threads) et bloquera jusqu'à ce que la boucle soit terminée, et Task.Factory créera explicitement une nouvelle instance de tâche pour chaque élément, et retournera avant qu'ils ne soient terminés (tâches asynchrones). Parallel.Foreach est beaucoup plus efficace.

Sogger
la source
11

À mon avis, le scénario le plus réaliste est lorsque les tâches doivent être exécutées de manière intensive. L'approche de Shivprasad se concentre davantage sur la création d'objets / l'allocation de mémoire que sur le calcul lui-même. J'ai fait une recherche appelant la méthode suivante:

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

L'exécution de cette méthode prend environ 0,5 seconde.

Je l'ai appelé 200 fois en utilisant Parallel:

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

Ensuite, je l'ai appelé 200 fois en utilisant l'ancienne méthode:

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

Le premier cas a été achevé en 26656 ms, le second en 24478 ms. Je l'ai répété plusieurs fois. Chaque fois que la deuxième approche est marginalement plus rapide.

user1089583
la source
Utiliser Parallel.For est l'ancienne méthode. L'utilisation de Tâche est recommandée pour les unités de travail qui ne sont pas uniformes. Les MVP de Microsoft et les concepteurs du TPL mentionnent également que l'utilisation des tâches utilisera les threads plus efficacement, ce qui en bloquera autant en attendant la fin des autres unités.
Suncat2000