Warum ist Parallel.ForEach viel schneller dann AsParallel().ForAll (), obwohl MSDN anderes vermuten?
Ich mache ein paar Untersuchungen um zu sehen, wie wir können, erstellen Sie eine Multithread-Anwendung, die durch einen Baum.
Zu finden, wie diese umgesetzt werden können, in der beste Weg, ich habe eine test-Anwendung, die läuft durch meine C:\ Festplatte und öffnet alle Verzeichnisse.
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.
}
}
}
Was ich je erlebt habe ist jedoch, dass bei der Ausführung dieser im Modus 3 (Parallel -.ForEach) der code abgeschlossen in rund 2,5 Sekunden (ja ich habe eine SSD 😉 ). Der code ausgeführt wird ohne Parallelisierung, die es vervollständigt in rund 8 Sekunden. Und der code ausgeführt wird im Modus 2 (AsParalle.ForAll()) dauert es eine nahezu unendliche Menge an Zeit.
Beim Check-in Prozess explorer I auch auftreten, ein paar seltsame Fakten:
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
Was ich besonders seltsam ist, dass Parallel.ForEach scheint zu ignorieren, die übergeordneten threads/tasks, die ausgeführt werden, während AsParallel().ForAll() scheint zu warten, bis der Vorherige Task entweder komplett (was so schnell nicht, da alle übergeordneten Aufgaben warten noch auf Ihr Kind Aufgaben zu vervollständigen).
Auch das, was ich Lesen Sie auf der MSDN-Website wurde: "Lieber ForAll zu ForEach, Wenn Es Möglich Ist"
Quelle: http://msdn.microsoft.com/en-us/library/dd997403(v=vs. 110).aspx
Hat jemand eine Ahnung, warum dies sein könnte?
Edit 1:
Anfrage von Matthew Watson ich habe zum ersten mal geladen wird der Baum in den Arbeitsspeicher, bevor die Schleife durch Sie. Nun die Belastung der Struktur erfolgt sequentiell.
Die Resultate jedoch sind die gleichen. Unparallelized und Parallel.ForEach schließen Sie nun den ganzen Baum in etwa 0,05 Sekunden, während AsParallel().ForAll noch geht nur um 1 Schritt pro Sekunde.
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:
Nach dem Lesen der update auf das Matthäus-Kommentar, ich habe versucht, fügen Sie den folgenden code, um das Programm:
ThreadPool.SetMinThreads(4000, 16);
ThreadPool.SetMaxThreads(4000, 16);
Diese jedoch nicht ändern, wie die AsParallel peforms. Immer noch die ersten 8 Schritte ausgeführt werden, in einem Augenblick, bevor Verlangsamung um 1 Schritt /Sekunde.
(Extra Hinweis, ich bin derzeit ignorieren die Ausnahmen, die auftreten, wenn ich keinen Zugriff auf ein Verzeichnis durch die Try-Catch-block um das Verzeichnis.GetDirectories())
Edit 3:
Auch das, was ich bin vor allem interessiert, ist der Unterschied zwischen Parallel.ForEach und AsParallel.Füralle, weil für mich ist es nur seltsam, dass aus irgendeinem Grund die zweite erzeugt einen Thread für jede Rekursion es tut, während die erste einmal verarbeitet, alles in rund 30 threads max. (Und auch, warum MSDN empfiehlt die AsParallel obwohl es schafft so viel threads mit ~1 Sekunde timeout)
Edit 4:
Andere seltsame Sache, die ich herausgefunden:
Wenn ich versuche, um die MinThreads auf den Thread-pool oberhalb von 1023 scheint es zu ignorieren, den Wert und die Skala wieder um 8-oder 16:
ThreadPool.SetMinThreads - (1023, 16);
Immer noch, wenn ich 1023 tut es die ersten 1023 Elemente sehr schnell folgte, gehen Sie wieder auf das langsame Tempo habe ich schon erlebt, die ganze Zeit.
Hinweis: Auch buchstäblich mehr als 1000 threads erstellt (im Vergleich zu 30 für die ganze Parallele.ForEach-one).
Bedeutet dies Parallel.ForEach ist einfach viel schlauer im Umgang mit Aufgaben?
Paar mehr Infos, dieser code druckt die zweimal 8 - 8 wenn Sie den Wert über 1023: (Wenn Sie die Werte bis 1023 oder niedriger, es gibt den richtigen Wert)
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:
Als der Dekan den Antrag habe ich einen anderen Fall manuell erstellen Aufgaben:
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;
Dieser ist auch schneller als die Parallel.ForEach () - Schleife. So haben wir immer noch nicht die Antwort, warum AsParallel().ForAll() ist so viel langsamer.
- Mit
ThreadPool.SetMinThreads(4000, 4000);
Sie die Einstellung der IO-completion-port-threads, um eine wahnsinnige Anzahl. VersuchenThreadPool.SetMinThreads(4000, 16);
statt (gleich fürSetMaxThreads()
) - Ich habe getan, dass jetzt aber irgendwie immer noch die gleichen Ergebnisse. Für Mode 2 sehe ich 1 extra-thread, oben knallend in den Ressourcen-monitor, jede Sekunde. Auch wenn ich Sie aktivieren die Konsole.WriteLine ich es sehe, bewegt sich durch meine Festplatte auf über 1 Stufe pro Sekunde. Modus 1 und 3 noch ausführen, meine ganze Festplatte (80.173-Elemente) in viel weniger als 1 Sekunde (die im Gedächtnis)
- Sie können verwenden Sie einen Task-orientierten version – was bedeutet, dass Sie erwarten, die Aufgabe, einen rekursiven Aufruf, und nicht die kick-off-eine neue Aufgabe, bis der Anruf beendet wurde? Das problem hier ist, dass Sie schnell überschwemmen die Anzahl der threads, um eine große Zahl, wenn Sie nur so etwas wie 4 CPU-Kerne, wenn dieses problem ist in-memory-und CPU-gebunden.
- Ich habe eine 4. Fall testen Sie Ihre Lösung fanden aber auch heraus, dass diese genauso schnell wie Parallel.ForEach. Ich glaube nicht, es hat viel mit Erinnerung zu tun-Operationen und CPU, da sehe ich fast keine CPU-Aktivität. Ich denke, es ist eher mit einer Art timeout irgendwo in der AsParallel().ForAll () - Methode.
- Das ist immer noch kicking off alle threads, die beim Start, denke ich. Ich habe nicht gedacht, dass dieser genau, aber ich dachte, Sie würde eine Wartezeit von einer Art innerhalb der for-Schleife, sodass die Schleife wird nicht fortgesetzt, bis die Rekursion erfolgt. Sie bekommen eine Menge von Tiefe und vielleicht mehrere threads, wie es geht nach unten das Verzeichnis, aber zumindest wäre es nicht schaffen, alle threads in einem Rutsch. Sie können auch untersuchen, die threads mit Thread.Aktuelle.ID, vielleicht aufzeichnen, wie viele threads erstellt werden, mit jeder Art von Modell.
- Ich weiß, es tut, aber das ist das ganze problem. Doing es auf diese Weise nicht zu verlangsamen. Es ist wirklich schnell. Nur Modus 2 (mit AsParallel().ForAll() verlangsamt, um 1 Schritt pro Sekunde)
- ja sorry, ich war off auf einer Tangente, die es gibt. Meine Beobachtungen mit den Aufgaben und mit den Parallelen.ForEach-Methoden ist, dass Sie nie mehr als eine Handvoll threads, in der Regel etwa 4 oder 8 mit hyperthreading auf einem quad-core-Maschine. Dann wieder, ich habe nie gewagt, Wagen, in eine recusive Untersuchung!
- yeah das ist, was ich scheinen zu erleben, als auch für Parallel.ForEach und neue Aufgaben (auch wenn Sie mit Rekursion). Ich hoffe das hier jemand erklären kann, warum und wie AsParallel.ForAll unterscheidet sich von Ihnen. (Und warum Microsoft nicht empfehlen die Verwendung von AsParallel.ForAll)
- Wenn Sie möchten, Parallel.Foreach in ein nicht CPU-bound tasks verwenden MaxDegreeOfParallelism = num_of_cores es wird Ressourcen zu sparen und schneller sein. in der AsParallel.ForAll warnt es nicht zu verwenden, es mit IO-bound-Aufgaben.
- Unabhängig von der Frage, aber Sie haben eine closure problem mit Fall 4 oben stackoverflow.com/questions/271440/...
- Ich werde es zu beheben, danke 🙂
Du musst angemeldet sein, um einen Kommentar abzugeben.
Dieses problem ist ziemlich debugfähiger, ein seltener Luxus, wenn Sie Probleme mit threads. Ihre grundlegende Werkzeug, hier ist der Fehler - > Windows > Threads debugger-Fenster. Zeigt Ihnen die aktiven threads und gibt Ihnen einen Blick auf Ihre stack-trace. Du wirst leicht sehen, dass, sobald es etwas langsam wird, dass Sie Dutzende threads aktiv, die sind alle geklebt. Ihre stack-trace alle gleich Aussehen:
Wenn man soetwas sieht, sollte man sofort denken Feuer-Schlauch-problem. Wahrscheinlich der Dritte-die häufigsten Fehler mit den Fäden, nach den Rennen und deadlocks.
Denen Sie Grund heraus, nun, Sie wissen, die Ursache, das problem mit dem code ist, dass jeder thread, der abgeschlossen ist, fügt N mehr threads. Wobei N die Durchschnittliche Anzahl der Unterverzeichnisse in einem Verzeichnis. Im Effekt, die Anzahl der Themen wächst exponentiell, das ist immer schlecht. Es wird nur die Kontrolle behalten, wenn N = 1, dass das natürlich nie passiert auf einer typischen Festplatte.
Tun, Bedenken Sie, dass, wie bei fast allen threading-problem, dass dieses Fehlverhalten neigt zu wiederholen, schlecht. Die SSD in deinem Rechner dazu neigt, es zu verbergen. So funktioniert der RAM in Ihrem Rechner haben, mag das Programm gut füllen Sie schnell und problemlos die zweite Zeit, die Sie ausführen. Da wirst du jetzt Lesen aus der Datei system cache statt von der Festplatte, sehr schnell. Basteln mit ThreadPool.SetMinThreads - () versteckt es so gut, aber es kann nicht reparieren. Es ist nie behebt ein problem, in der es nur versteckt Sie. Denn egal was passiert, die exponentielle Anzahl wird immer überwältigen die eingestellte minimale Anzahl an threads. Man kann nur hoffen, dass es abgeschlossen Beendigung des Durchlaufs das Laufwerk, bevor das passiert. Im Leerlauf Hoffnung für einen Benutzer mit einer großen Festplatte.
Den Unterschied zwischen ParallelEnumerable.ForAll() und Parallel.ForEach() ist jetzt vielleicht auch leicht erklärt. Können Sie sagen, von den stack-trace ForAll() tut etwas frech, die RunSynchronously () - Methode blockiert, bis alle threads beendet sind. Die Blockierung ist etwas threadpool-threads nicht tun sollte, es Zahnfleisch bis der thread-pool und erlauben es nicht planen, den Prozessor für einen anderen job. Und hat den Effekt, den Sie beobachtet werden, wird der thread-pool ist schnell überfordert mit threads, die warten auf die N anderen threads abgeschlossen ist. Das ist nicht passiert, Sie warten darauf, in den pool und sind nicht immer geplant werden, da gibt es schon so viele von Ihnen aktiv.
Dies ist eine deadlock-Szenario, ein ziemlich gewöhnlicher, sondern der threadpool-manager hat eine Abhilfe für Sie. Es sieht den active-threadpool-threads und Maßnahmen, wenn Sie nicht in einer fristgerechten Weise. Es ermöglicht dann eine extra thread zu starten, in der man mehr als die minimale Menge von SetMinThreads - (). Aber nicht mehr dann ist der maximale Satz von SetMaxThreads(), dass zu viele aktive tp-threads ist riskant und wahrscheinlich trigger OOM. Dies macht das lösen der Blockade, wird es eine der ForAll () - Aufrufe abgeschlossen. Aber dies geschieht auf einer sehr langsamen Geschwindigkeit, die threadpool-nur wird dies zweimal pro Sekunde. Sie laufen aus Geduld, bevor es wieder aufholt.
Parallel.ForEach() dieses problem nicht hat, es nicht zu blockieren, damit nicht Gummi-up-pool.
Scheint die Lösung zu sein, aber Bedenken Sie, dass Ihr Programm ist immer noch Feuer-Abspritzen die Erinnerung an Ihre Maschine, hinzufügen von immer mehr warten tp-threads zum pool. Dies kann zum Absturz Ihres Programms als gut, es ist einfach nicht so wahrscheinlich, weil Sie eine Menge Speicher und den threadpool verwenden, eine Menge, um zu verfolgen eine Anfrage. Einige Programmierer aber erreichen, dass auch.
Die Lösung ist eine sehr einfache, die einfach nicht mit threading. Es ist schädlich, es gibt keine Parallelität, wenn Sie nur eine Festplatte. Und es tut nicht wie requiriert durch mehrere threads. Besonders schlimm auf einer Spindel, Laufwerk, Kopf, sucht, sehr, sehr, sehr langsam. SSDs machen es viel besser, es jedoch trotzdem eine einfache 50 Mikrosekunden overhead, die Sie gerade nicht wollen oder brauchen. Die ideale Anzahl der threads, Zugriff auf eine Festplatte, dass kann man nicht anders erwarten, werden Cache ist immer eine.
Das erste, was zu beachten ist, dass Sie versuchen, zu parallelise ein IO-bound-Vorgang, die verzerren die timings deutlich.
Das zweite, was zu beachten ist die Art der parallelised Aufgaben: Sie sind rekursiv absteigende einem directory-Baum. Wenn Sie erstellen mehrere threads dazu, jeder thread wird wahrscheinlich auf einem anderen Teil der Festplatte, die gleichzeitig die bewirkt, dass der Festplatten-Lesekopf springen alle über den Ort und Verlangsamung deutlich.
Versuchen Sie, Ihren test zu erstellen, eine in-memory-Baum, und Zugang, die mit mehreren threads statt. Dann werden Sie in der Lage zu vergleichen, die timings werden richtig ohne die Ergebnisse verfälscht, jenseits aller Nützlichkeit.
Darüber hinaus können Sie schaffen eine große Anzahl von threads, und Sie werden (per default) werden die threadpool-threads. Mit einer großen Anzahl von threads wird eigentlich die Dinge verlangsamen, wenn Sie mehr als die Anzahl der Prozessor-Kerne.
Beachten Sie auch, dass, wenn Sie mehr als die thread-pool mindestens threads (definiert durch
ThreadPool.GetMinThreads()
), eine Verzögerung eingeführt wird, die durch den thread-pool-manager zwischen jedem neuen threadpool-thread erstellen. (Ich denke, das ist um 0,5 s pro neuer thread).Auch, wenn die Anzahl der threads übersteigt der Wert, der zurückgegeben wird, durch
ThreadPool.GetMaxThreads()
die Schaffung thread wird blockiert, bis einer der anderen threads beendet wurde. Ich denke, das ist wahrscheinlich passiert zu sein.Können Sie testen diese Hypothese, indem Sie den Aufruf
ThreadPool.SetMaxThreads()
undThreadPool.SetMinThreads()
diese Werte zu erhöhen, und sehen, ob es einen Unterschied macht.(Beachten Sie abschließend, dass, wenn Sie wirklich versuchen, rekursiv absteigen von
C:\
Sie wird mit ziemlicher Sicherheit erhalten Sie ein E /a-Ausnahme, wenn es erreicht eine geschützte Betriebssystem-Ordner.)HINWEIS: Legen Sie die max/min-threadpool-threads wie diesen:
Follow-Up
Ich habe versucht, Ihre test-code mit dem threadpool-thread counts eingestellt, wie oben beschrieben, mit den folgenden Ergebnissen (laufen nicht auf meinem Laufwerk C:\, sondern auf eine kleinere Teilmenge):
Dies ist im Einklang mit meinen Erwartungen, das hinzufügen einer Last von threading zu tun diese eigentlich macht es langsamer als single-threaded ist, und die zwei parallele Ansätze nehmen in etwa die gleiche Zeit.
Falls jemand anderes will, um das zu prüfen, hier ein paar bestimmend test-code (der OP-code ist nicht reproduzierbar, weil wir nicht wissen, seine Verzeichnis-Struktur).
List
(26.3 Sek.), einIEnumerable<string>
mit einemToList
(26.3 Sek.) und eineIEnumerable<string>
mit einemToList
hat, der die Rekursion mitParallel.ForEach
(8.5 Sek.). DieParallel.ForEach
ist 4 mal schneller.Den Parallelen.Für die und .ForEach-Methoden implementiert, die intern als gleichwertig zu laufen Iterationen Aufgaben, wie z.B. die, dass eine Schleife wie:
ist äquivalent zu:
Und aus der Perspektive jeder iteration potenziell parallel läuft mit jeder weiteren iteration, dies ist ein ok - geistige Modell, aber nicht passieren inreality. Parallel, in der Tat, nicht unbedingt verwenden Sie eine Aufgabe pro iteration, wie das ist deutlich mehr Aufwand, als notwendig ist. Parallel.ForEach versucht, verwenden Sie die minimale Anzahl von Aufgaben, die nötig sind, um die Schleife so schnell wie möglich. Es dreht Aufgaben als threads verfügbar sind zum verarbeiten der Aufgaben und jede dieser Aufgaben beteiligt sich über ein management-System (ich denke, die genannt chunking): Eine Aufgabe, die fordert mehrere Iterationen durchgeführt werden, bekommt Sie, und dann Prozesse, dass die Arbeit, und dann geht zurück für mehr. Die Blockgrößen variieren die Anzahl der Aufgaben, die teilnehmenden, die Last der Maschine, etc.
PLINQ ist .AsParallel() hat eine andere Umsetzung, aber es 'können' noch ähnlich fetch mehrere Iterationen in einem temporären Speicher, führen Sie die Berechnungen in einem thread (nicht aber als eine Aufgabe), und legen Sie die Ergebnisse der Abfrage in einem kleinen Puffer. (Sie bekommen etwas, was sich auf ParallelQuery, und dann weiter .Was auch immer () - Funktionen binden, um einen alternativen Satz von erweiterungsmethoden, dass Sie eine parallele Implementierungen).
So, jetzt haben wir eine kleine Vorstellung davon, wie diese beiden Mechanismen arbeiten, ich werde versuchen, eine Antwort zu deiner ursprünglichen Frage:
So warum ist .AsParallel() langsamer als die Parallele.ForEach? Der Grund ergibt sich aus dem folgenden. Aufgaben (oder gleichwertige Umsetzung hier) tun NICHT block auf der I/O-wie Anrufe. Sie 'erwarten' und die CPU etwas anderes tun. Aber (Zitat aus C# nutshell-Buch): "PLINQ nicht durchführen können I/O-gebundene Arbeit, ohne threads zu blockieren". Die Anrufe werden synchrone. Sie wurden mit der Absicht geschrieben, dass Sie erhöhen den Grad der Parallelität, wenn (und NUR wenn) Sie tun solche Dinge wie das herunterladen von web-Seiten pro task, die nicht hog CPU-Zeit.
Und der Grund, warum Ihre Funktion fordert, sind genau Analog-I/O-bound Anrufe, ist dies: Einen deiner threads (nennen wir ihn T) blockiert und tut nichts, bis alle seine Kind-Prozesse beendet haben, das kann ein langsamer Prozess hier. T selbst ist nicht CPU-intensiv ist, während es wartet, bis die Kinder wieder zu entsperren, es ist nichts tun, als warten. Daher ist es identisch zu einer typischen I/O-bound-Funktion aufrufen.
Basierend auf der akzeptierten Antwort auf Wie genau funktioniert AsParallel Arbeit?
.AsParallel.ForAll()
wirft zurückIEnumerable
vor dem Aufruf.ForAll()
so schafft es 1 neuen thread + N rekursive Aufrufe (von denen jeder erzeugt einen neuen thread).