Pourquoi est Parallèle.ForEach beaucoup plus vite alors AsParallel().ForAll() même si MSDN suggère le contraire?
J'ai fait quelques recherches pour voir comment nous pouvons créer une application multi-thread qui s'exécute par le biais d'un arbre.
De trouver comment cela peut être mis en œuvre de la meilleure façon que j'ai créé un test d'application qui s'exécute par le biais de mon C:\ disque et qui ouvre tous les répertoires.
class Program
{
static void Main(string[] args)
{
//var startDirectory = @"C:\The folder\RecursiveFolder";
var startDirectory = @"C:\";
var w = Stopwatch.StartNew();
ThisIsARecursiveFunction(startDirectory);
Console.WriteLine("Elapsed seconds: " + w.Elapsed.TotalSeconds);
Console.ReadKey();
}
public static void ThisIsARecursiveFunction(String currentDirectory)
{
var lastBit = Path.GetFileName(currentDirectory);
var depth = currentDirectory.Count(t => t == '\\');
//Console.WriteLine(depth + ": " + currentDirectory);
try
{
var children = Directory.GetDirectories(currentDirectory);
//Edit this mode to switch what way of parallelization it should use
int mode = 3;
switch (mode)
{
case 1:
foreach (var child in children)
{
ThisIsARecursiveFunction(child);
}
break;
case 2:
children.AsParallel().ForAll(t =>
{
ThisIsARecursiveFunction(t);
});
break;
case 3:
Parallel.ForEach(children, t =>
{
ThisIsARecursiveFunction(t);
});
break;
default:
break;
}
}
catch (Exception eee)
{
//Exception might occur for directories that can't be accessed.
}
}
}
Ce que j'ai rencontrés, cependant, est que lors de l'exécution de cette dans le mode 3 (Parallèle.ForEach) le code se termine dans environ 2,5 secondes (oui, j'ai un SSD 😉 ). L'exécution du code, sans parallélisation il termine en environ 8 secondes. Et l'exécution du code dans le mode 2 (AsParalle.ForAll ()), il faut une quasi-infinie quantité de temps.
Lors de la vérification dans le processus de l'explorateur j'ai aussi rencontrer quelques faits étranges:
Mode1 (No Parallelization):
Cpu: ~25%
Threads: 3
Time to complete: ~8 seconds
Mode2 (AsParallel().ForAll()):
Cpu: ~0%
Threads: Increasing by one per second (I find this strange since it seems to be waiting on the other threads to complete or a second timeout.)
Time to complete: 1 second per node so about 3 days???
Mode3 (Parallel.ForEach()):
Cpu: 100%
Threads: At most 29-30
Time to complete: ~2.5 seconds
Ce que je trouve particulièrement étrange, c'est qu'en Parallèle.ForEach semble ignorer tout parent threads/tâches qui sont toujours en cours d'exécution tandis que AsParallel().ForAll() semble attendre que la Tâche précédente soit terminée (ce qui n'est pas bientôt puisque tous les parents de Tâches sont toujours en attente de leur enfant tâches à accomplir).
Aussi ce que j'ai lu sur MSDN était: "Préférez ForAll ForEach Quand C'Est Possible"
Source: http://msdn.microsoft.com/en-us/library/dd997403(v=vs. 110).aspx
Quelqu'un a une idée pourquoi cela pourrait être?
Edit 1:
Comme demandé par Matthew Watson, j'ai d'abord chargé de l'arbre en mémoire avant de faire la boucle à travers elle. Maintenant, le chargement de l'arbre se fait de manière séquentielle.
Les résultats sont cependant les mêmes. Unparallelized et Parallèles.ForEach maintenant compléter l'ensemble de l'arbre à environ 0,05 secondes, tandis que AsParallel().Pourtout encore ne va autour de 1 étape par seconde.
Code:
class Program
{
private static DirWithSubDirs RootDir;
static void Main(string[] args)
{
//var startDirectory = @"C:\The folder\RecursiveFolder";
var startDirectory = @"C:\";
Console.WriteLine("Loading file system into memory...");
RootDir = new DirWithSubDirs(startDirectory);
Console.WriteLine("Done");
var w = Stopwatch.StartNew();
ThisIsARecursiveFunctionInMemory(RootDir);
Console.WriteLine("Elapsed seconds: " + w.Elapsed.TotalSeconds);
Console.ReadKey();
}
public static void ThisIsARecursiveFunctionInMemory(DirWithSubDirs currentDirectory)
{
var depth = currentDirectory.Path.Count(t => t == '\\');
Console.WriteLine(depth + ": " + currentDirectory.Path);
var children = currentDirectory.SubDirs;
//Edit this mode to switch what way of parallelization it should use
int mode = 2;
switch (mode)
{
case 1:
foreach (var child in children)
{
ThisIsARecursiveFunctionInMemory(child);
}
break;
case 2:
children.AsParallel().ForAll(t =>
{
ThisIsARecursiveFunctionInMemory(t);
});
break;
case 3:
Parallel.ForEach(children, t =>
{
ThisIsARecursiveFunctionInMemory(t);
});
break;
default:
break;
}
}
}
class DirWithSubDirs
{
public List<DirWithSubDirs> SubDirs = new List<DirWithSubDirs>();
public String Path { get; private set; }
public DirWithSubDirs(String path)
{
this.Path = path;
try
{
SubDirs = Directory.GetDirectories(path).Select(t => new DirWithSubDirs(t)).ToList();
}
catch (Exception eee)
{
//Ignore directories that can't be accessed
}
}
}
Edit 2:
Après la lecture de la mise à jour sur Matthieu commentaire, j'ai essayé d'ajouter le code suivant au programme:
ThreadPool.SetMinThreads(4000, 16);
ThreadPool.SetMaxThreads(4000, 16);
Toutefois, cela ne change pas la façon dont le AsParallel peforms. Toujours les 8 premières étapes sont exécutées dans un instant, avant de ralentir à 1 étape /seconde.
(Note, je suis actuellement en ignorant les exceptions qui se produisent quand je ne peux pas accéder à un Répertoire par le bloc Try Catch autour de l'Annuaire.GetDirectories())
Edit 3:
Aussi ce que je m'intéresse principalement à la différence entre le Parallèle.ForEach et AsParallel.Pourtout parce que pour moi c'est juste étrange que, pour une raison quelconque, le second crée un Thread pour chaque récursion il le fait alors que le premier s'occupe de tout en autour de 30 fils max. (Et aussi pourquoi MSDN suggère d'utiliser le AsParallel même si elle crée tant de discussions avec un ~1 seconde de délai d'attente)
Edit 4:
Une autre chose étrange, j'ai trouvé:
Lorsque je tente de régler le MinThreads sur le pool de Threads au-dessus de 1023 il semble ignorer la valeur et de l'échelle de retour à environ 8 ou 16:
ThreadPool.SetMinThreads(1023, 16);
Encore lorsque j'utilise le 1023 il ne la première 1023 éléments très vite suivie par un retour à la lenteur avec laquelle j'ai eu l'expérience de tous les temps.
Note: littéralement plus de 1000 sujets sont maintenant créés (contre 30 pour l'ensemble de la Parallèle.ForEach un).
Est-ce à dire Parallèle.ForEach est juste plus intelligents dans les tâches de manipulation?
Quelques infos, ce code imprime deux fois 8 - 8 lorsque vous définissez la valeur au-dessus de 1023: (Lorsque vous définissez les valeurs à 1023 ou inférieur, il imprime la valeur correcte)
int threadsMin;
int completionMin;
ThreadPool.GetMinThreads(out threadsMin, out completionMin);
Console.WriteLine("Cur min threads: " + threadsMin + " and the other thing: " + completionMin);
ThreadPool.SetMinThreads(1023, 16);
ThreadPool.SetMaxThreads(1023, 16);
ThreadPool.GetMinThreads(out threadsMin, out completionMin);
Console.WriteLine("Now min threads: " + threadsMin + " and the other thing: " + completionMin);
Edit 5:
En tant que Doyen de la demande, j'ai créé un autre cas de créer manuellement des tâches:
case 4:
var taskList = new List<Task>();
foreach (var todo in children)
{
var itemTodo = todo;
taskList.Add(Task.Run(() => ThisIsARecursiveFunctionInMemory(itemTodo)));
}
Task.WaitAll(taskList.ToArray());
break;
C'est aussi rapide qu'avec le Parallèle.ForEach() en boucle. Donc, nous n'avons toujours pas la réponse à pourquoi AsParallel().ForAll() est beaucoup plus lent.
- Avec
ThreadPool.SetMinThreads(4000, 4000);
vous êtes réglage de l'IO port de terminaison des threads pour un nombre fou. EssayezThreadPool.SetMinThreads(4000, 16);
à la place (même pourSetMaxThreads()
) - Je l'ai fait maintenant, mais de toute façon toujours éprouver les mêmes résultats. Pour le Mode 2, je vois 1 fil supplémentaire popping up dans le moniteur de Ressources à chaque seconde. Aussi, quand j'permettre à la Console.WriteLine je le vois passer au travers de mon disque à environ 1 par seconde. Le Mode 1 et le 3, toujours exécuter l'ensemble de mon disque (80.173 éléments) en moins de 1 seconde (dans la mémoire de l'un)
- Pouvez-vous utiliser une Tâche orientée vers la version – sens qui vous attendent à la mission d'un appel récursif, et de ne pas coup d'envoi d'une nouvelle tâche jusqu'à ce que l'appel est terminé? Le problème ici c'est que vous êtes rapidement inonder le nombre de threads à un grand nombre, quand vous avez quelque chose comme 4 cœurs du PROCESSEUR lorsque ce problème est en mémoire et CPU.
- J'ai créé un 4e cas à tester votre solution, mais aussi découvert que c'est tout aussi rapide que l'Parallèle.ForEach. Je ne pense pas que cela a beaucoup à voir avec les opérations de mémoire et de CPU parce que je ne vois presque aucune activité du PROCESSEUR. Je pense que c'est plus lié à une sorte de délai d'attente quelque part dans le AsParallel().ForAll() la méthode.
- C'est encore le coup d'envoi de tous les threads au démarrage, je pense. Je ne pensais pas ça exactement, mais je pensais que tu mettrais une attente de quelque sorte à l'intérieur de la boucle, de sorte que la boucle ne pas continuer jusqu'à ce que la récursivité est fait. Vous obtenez beaucoup de profondeur et peut-être plusieurs threads qu'il parcourt l'arborescence du répertoire, mais au moins il ne serait pas de créer tous les threads d'un seul coup. Vous pouvez également enquêter sur les fils à l'aide de Fil.Actuel.ID, peut-être, notez le nombre de threads sont créés avec chaque type de modèle.
- Je sais qu'il n', mais c'est tout le problème. Faire de cette façon n'est pas le ralentir. Elle est très rapide. Uniquement en Mode 2 (avec AsParallel().ForAll() ralentit à l'étape 1 par seconde)
- oui désolé, j'étais sur une tangente là. Mes observations avec les Tâches et les Parallèles.ForEach méthodes est qu'elles ne créer plus qu'une poignée de fils, généralement autour de 4 ou 8 avec l'hyperthreading sur un quad core de la machine. Encore une fois, je n'ai jamais osé s'aventurer dans un recusive enquête!
- ouais c'est ce que je semble faire l'expérience aussi bien en Parallèle.ForEach et de nouvelles Tâches (même lors de l'utilisation de la récursivité). J'espère que quelqu'un ici peut expliquer le pourquoi et le comment AsParallel.Pourtout en diffère. (Et pourquoi Microsoft ne suggérons d'utiliser AsParallel.Pourtout)
- Si vous voulez en Parallèle.Foreach dans un non le CPU, la tâche de l'utilisation MaxDegreeOfParallelism = num_of_cores il vous fera économiser des ressources et d'être plus rapides. dans le AsParallel.Pourtout il met en garde de ne pas l'utiliser avec IO des tâches limitées.
- Aucun rapport avec la question, mais vous avez un problème avec fermeture cas 4 ci-dessus stackoverflow.com/questions/271440/...
- Je vais corriger ça merci 🙂
Vous devez vous connecter pour publier un commentaire.
Ce problème est assez debuggable, rare, de luxe lorsque vous avez des problèmes avec les threads. Votre outil de base ici est le Debug > Windows > les Fils fenêtre du débogueur. Vous montre les threads actifs et vous donne un coup d'oeil à leur trace de la pile. Vous pourrez facilement voir que, une fois qu'il est lent, que vous aurez des dizaines de threads actifs qui sont tous coincés. Leur trace de la pile se ressemblent tous:
Chaque fois que vous voyez quelque chose comme cela, vous devez immédiatement penser de boyaux d'incendie problème. Probablement la troisième plus courantes d'un bug avec les threads, après les courses et les blocages.
Que vous pouvez raison, maintenant que vous connaissez la cause, le problème avec le code, c'est que chaque thread se termine ajoute N plus de threads. Où N est le nombre moyen de sous-répertoires dans un répertoire. En effet, le nombre de threads pousse de façon exponentielle, qui est toujours mauvais. Il ne garder le contrôle si N = 1, ce qui bien sûr n'arrive jamais sur un disque classique.
Faire attention que, comme presque tout le filetage problème, que ce mauvais comportement tend à se répéter mal. Le SSD dans votre machine a tendance à le cacher. Donc, la mémoire vive de votre machine, le programme pourrait bien remplir rapidement et sans problème la deuxième fois que vous l'exécutez. Puisque vous allez maintenant lire à partir du cache du système de fichiers au lieu du disque, très rapide. Bricoler avec de pool de threads.SetMinThreads() cache bien, mais il ne peut pas le réparer. Jamais il ne résout aucun problème, il ne les cache. Parce que peu importe ce qui se passe, le nombre exponentiel toujours submerger le minimum nombre de threads. Vous pouvez seulement espérer qu'il complète la finition de l'itération de la voiture avant que cela arrive. Espoir vain pour un utilisateur avec un gros lecteur.
La différence entre ParallelEnumerable.ForAll() et Parallèle.ForEach() est peut-être maintenant aussi facile à expliquer. Vous pouvez dire à partir de la trace de la pile qui ForAll() fait quelque chose de méchant, le RunSynchronously() la méthode des blocs jusqu'à ce que tous les fils sont terminés. Le blocage est quelque chose de pool de threads les threads ne faut pas faire, il gencives jusqu'le pool de threads et ne permettra pas à programmer le processeur pour un autre emploi. Et a l'effet que vous avez observé, le pool de threads est vite débordé avec les threads en attente sur la N d'autres threads pour terminer. Ce qui n'est pas le cas, ils sont en attente dans la piscine et ne reçoivent pas prévu, parce que il y a déjà un si grand nombre d'entre elles sont actives.
C'est une situation de blocage, assez commun, mais le pool de threads manager est une solution de contournement pour elle. Il veille active de pool de threads les threads et les étapes quand ils ne sont pas complètes en temps opportun. Il permet ensuite à un supplémentaire thread pour commencer, un de plus que le salaire minimum fixé par SetMinThreads(). Mais pas plus que le maximum fixé par SetMaxThreads(), d'avoir un trop grand nombre d'actifs tp fils est risqué et susceptible de déclencher OOM. Ce n'résoudre l'impasse, il obtient l'un des ForAll() appelle à remplir. Mais ce qui se passe à un rythme très lent, le pool de threads seulement deux fois par seconde. Vous serez à court de patience avant qu'il ne la rattrape.
Parallèle.ForEach() n'ont pas ce problème, il n'a pas de bloc n'est pas la gomme jusqu'à la piscine.
Semble être la solution, mais gardez à l'esprit que votre programme est encore en feu-quittance de la mémoire de votre machine, en ajoutant toujours plus d'attente tp fils à la piscine. Cela peut faire planter votre programme ainsi, il n'est pas probable parce que vous avez beaucoup de mémoire et le pool de threads n'utilise pas beaucoup pour garder la trace d'une demande. Certains programmeurs cependant obtenir aussi bien.
La solution est très simple, il suffit de ne pas utiliser le threading. Il est nuisibles, il n'y a pas de simultanéité lorsque vous avez un seul disque. Et il ne pas comme étant réquisitionné par plusieurs threads. Particulièrement mauvais sur un disque rotatif, chef cherche sont très, très lent. Les ssd font beaucoup mieux, toutefois, elle prend toujours facile 50 microsecondes, les frais généraux et que vous ne voulez pas ou besoin. L'idéal nombre de threads d'accéder à un disque que vous ne pouvez pas s'attendre à être mis en cache bien est toujours un.
La première chose à noter est que vous essayez de paralléliser un IO opération, qui risquent de déformer les horaires de manière significative.
La deuxième chose à noter est la nature de la paralléliser les tâches: Vous êtes à la descente récursive d'une arborescence. Si vous créez plusieurs threads pour ce faire, chaque thread est susceptible d'accéder à une autre partie du disque en même temps - qui sera la cause de la lecture du disque à la tête de se faire sauter dans tous les sens et ralentir les choses considérablement.
Essayez de changer votre test de créer une mémoire de l'arbre, et l'accès qu'avec plusieurs threads à la place. Ensuite, vous serez en mesure de comparer les horaires correctement et que les résultats ne soient déformées au-delà de toute utilité.
En outre, vous pouvez peut-être la création d'un grand nombre de threads, et ils seront (par défaut) être pool de threads les threads. Avoir un grand nombre de threads seront en fait ralentir les choses quand elles dépassent le nombre de cœurs de processeur.
Notez également que lorsque vous dépasser le pool de threads minimum de threads (défini par
pool de threads.GetMinThreads()
), un délai est introduite par le gestionnaire du pool de thread entre chaque nouveau pool de threads création de thread. (Je crois que c'est autour de 0,5 s par nouveau thread).Aussi, si le nombre de threads dépasse la valeur retournée par
ThreadPool.GetMaxThreads()
, la création de thread se bloque jusqu'à ce l'un de l'autre fils a quitté. Je pense que cela est susceptible de se produire.Vous pouvez tester cette hypothèse en appelant
ThreadPool.SetMaxThreads()
etThreadPool.SetMinThreads()
augmenter ces valeurs, et voir si cela fait une différence.(Enfin, notez que si vous êtes vraiment essayer de descendre de manière récursive à partir de
C:\
, vous aurez presque certainement obtenir une IO exception lorsqu'il atteint un protégé dossier du système d'exploitation.)REMARQUE: Réglez la valeur max/min de pool de threads les threads comme ceci:
Suivi
J'ai essayé votre code de test avec le pool de threads thread compte comme indiqué ci-dessus, avec les résultats suivants (ne pas fonctionner sur l'ensemble de mon disque C:\, mais sur un plus petit sous-ensemble):
Cela est conforme à mes attentes; l'ajout d'une charge de filetage pour ce faire, en fait, il est plus lent que monothread, et le parallèle deux approches prennent à peu près le même temps.
Au cas où quelqu'un d'autre veut étudier cette question, voici quelques déterminant le code de test (l'OP du code n'est pas reproductible car nous ne savons pas sa structure de répertoire).
List
(26.3 secs), unIEnumerable<string>
avec unToList
(26.3 secondes) et unIEnumerable<string>
avec unToList
qui ne la récursivité avecParallel.ForEach
(8.5 secondes). LeParallel.ForEach
est 4 fois plus rapide.Le Parallèle.Pour et .ForEach moyens sont mis en œuvre à l'interne comme équivalent à l'exécution d'itérations dans les Tâches, par exemple, qu'une boucle comme:
est équivalent à:
Et du point de vue de chaque itération, éventuellement en parallèle avec tous les autres itération, c'est un ok mentale modèle, mais ne se fait pas dans la réalité. En parallèle, en fait, ne pas nécessairement utiliser une Tâche par itération, comme c'est beaucoup plus de ressources que nécessaire. En parallèle.ForEach essaie d'utiliser au minimum le nombre de tâches nécessaires pour compléter la boucle aussi vite que possible. Il tourne des tâches en tant que fils deviennent disponibles pour traiter ces tâches, et chacune de ces tâches participe à un régime de gestion (je pense que sa prétendue chunking): Une tâche demande pour de multiples itérations à faire, les obtient, et puis des processus de travail, et puis s'en va revenir pour plus. Le morceau tailles varient en fonction du nombre de tâches participants, la charge sur la machine, etc.
De PLINQ .AsParallel (a), d'une mise en œuvre différente, mais il "peut" toujours de la même façon extraire plusieurs itérations dans un magasin temporaire, de faire les calculs dans un thread (mais pas une tâche), et de mettre les résultats de la requête dans un petit tampon. (Vous obtenez quelque chose de basé sur ParallelQuery, et puis d'autres .Quelle que soit la() des fonctions de lier à un autre ensemble de méthodes d'extension qui fournissent des implémentations parallèles).
Alors, maintenant que nous avons une petite idée de la façon dont ces deux mécanismes de travail, je vais essayer de donner une réponse à votre question de départ:
Alors pourquoi est .AsParallel() plus lent que Parallèle.ForEach? La raison provient de la suivante. Tâches (ou leur équivalent de la mise en œuvre ici) ne PAS sur le bloc d'e/S comme des appels. Ils "attendent" et libérer le PROCESSEUR pour faire autre chose. Mais (citant C# mot du livre): “PLINQ ne peut pas effectuer des e/S de travail sans blocage des threads”. Les appels sont synchrone. Ils ont été écrits avec l'intention que vous augmenter le degré de parallélisme si (et SEULEMENT si) vous faites des choses telles que le téléchargement de pages web par des tâches qui ne sont pas de porc de temps PROCESSEUR.
Et la raison pour laquelle vos appels de fonction sont exactement analogue à I/O bound appels est: est-ce l'Un de vos fils (que l'on appellera T) bloque et ne fait rien jusqu'à ce que tous ses threads enfants ont fini, qui peut être un processus lent ici. T lui-même n'est pas CPU-intensive pendant qu'il attend les enfants pour le débloquer, c'est de ne rien faire mais en attendant. Par conséquent, il est identique à un type de I/O bound appel de fonction.
Basé sur la accepté de répondre à Comment fonctionne exactement AsParallel travail?
.AsParallel.ForAll()
jette en arrière àIEnumerable
avant d'appeler.ForAll()
de sorte qu'il crée 1 nouveau thread + N appels récursifs (qui génère un nouveau thread).