Parallel.ForEach vs Task.Factory.StartNew

267

Was ist der Unterschied zwischen den folgenden Codefragmenten? Werden nicht beide Threadpool-Threads verwenden?

Wenn ich beispielsweise für jedes Element in einer Sammlung eine Funktion aufrufen möchte,

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

vs

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

Antworten:

302

Das erste ist eine viel bessere Option.

Parallel.ForEach verwendet intern a Partitioner<T>, um Ihre Sammlung in Arbeitselemente zu verteilen. Es wird nicht eine Aufgabe pro Element ausgeführt, sondern diese stapelweise ausgeführt, um den damit verbundenen Overhead zu verringern.

Mit der zweiten Option wird eine einzelne Taskpro Artikel in Ihrer Sammlung geplant. Während die Ergebnisse (fast) gleich sind, führt dies zu einem weitaus höheren Overhead als erforderlich, insbesondere bei großen Sammlungen, und führt dazu, dass die Gesamtlaufzeiten langsamer sind.

Zu Ihrer Information - Der verwendete Partitionierer kann gesteuert werden, indem die entsprechenden Überlastungen für Parallel.ForEach verwendet werden , falls dies gewünscht wird. Weitere Informationen finden Sie unter Benutzerdefinierte Partitionierer in MSDN.

Der Hauptunterschied zur Laufzeit besteht darin, dass der zweite asynchron wirkt. Dies kann mit Parallel.ForEach dupliziert werden, indem Sie Folgendes tun:

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

Auf diese Weise nutzen Sie weiterhin die Partitionierer, blockieren jedoch erst, wenn der Vorgang abgeschlossen ist.

Reed Copsey
quelle
8
IIRC, die von Parallel.ForEach durchgeführte Standardpartitionierung, berücksichtigt auch die Anzahl der verfügbaren Hardwarethreads, sodass Sie nicht die optimale Anzahl der zu startenden Aufgaben ermitteln müssen. Lesen Sie den Artikel zu Mustern der parallelen Programmierung von Microsoft . Es enthält großartige Erklärungen für all diese Dinge.
Mal Ross
2
@Mal: Irgendwie ... Das ist eigentlich nicht der Partitionierer, sondern die Aufgabe des TaskSchedulers. Der TaskScheduler verwendet standardmäßig den neuen ThreadPool, der dies jetzt sehr gut handhabt.
Reed Copsey
Vielen Dank. Ich wusste, ich hätte in der Einschränkung "Ich bin kein Experte, aber ..." gehen sollen. :)
Mal Ross
@ReedCopsey: Wie füge ich über Parallel.ForEach gestartete Aufgaben an die Wrapper-Aufgabe an? Wenn Sie also .Wait () für eine Wrapper-Aufgabe aufrufen, bleibt diese hängen, bis parallel ausgeführte Aufgaben abgeschlossen sind?
Konstantin Tarkus
1
@Tarkus Wenn Sie mehrere Anfragen stellen, ist es besser, nur HttpClient.GetString in jedem Arbeitselement (in Ihrer Parallelschleife) zu verwenden. Kein Grund, eine asynchrone Option in die bereits gleichzeitige Schleife einzufügen, normalerweise ...
Reed Copsey
89

Ich habe ein kleines Experiment durchgeführt, bei dem eine Methode "1.000.000.000 (eine Milliarde)" Mal mit "Parallel.For" und eine mit "Task" -Objekten ausgeführt wurde.

Ich habe die Prozessorzeit gemessen und Parallel effizienter gefunden. Parallel.Für unterteilt Ihre Aufgabe in kleine Arbeitselemente und führt sie auf allen Kernen parallel auf optimale Weise aus. Beim Erstellen vieler Aufgabenobjekte (FYI TPL verwendet intern das Thread-Pooling) wird jede Ausführung für jede Aufgabe verschoben, wodurch mehr Stress in der Box entsteht, was aus dem folgenden Experiment hervorgeht.

Ich habe auch ein kleines Video erstellt, das die grundlegende TPL erklärt und zeigt, wie Parallel.For Ihren Kern im Vergleich zu normalen Aufgaben und Threads effizienter nutzt. Http://www.youtube.com/watch?v=No7QqSc5cl8 .

Versuch 1

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

Experiment 2

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

Prozessorzeitvergleich

Shivprasad Koirala
quelle
Es wäre effizienter und der Grund dafür, dass das Erstellen von Threads kostspielig ist, ist Experiment 2 eine sehr schlechte Praxis.
Tim
@ Georgi-es ist mir wichtig, mehr darüber zu reden, was schlecht ist.
Shivprasad Koirala
3
Es tut mir leid, mein Fehler, ich hätte es klären sollen. Ich meine die Erstellung von Aufgaben in einer Schleife bis 1000000000. Der Overhead ist unvorstellbar. Ganz zu schweigen davon, dass die Parallele nicht mehr als 63 Aufgaben gleichzeitig erstellen kann, was sie in diesem Fall wesentlich optimierter macht.
Georgi-it
Dies gilt für 1000000000 Aufgaben. Wenn ich jedoch ein Bild verarbeite (wiederholt, fraktal zoomen) und Parallel mache. In Zeilen sind viele Kerne inaktiv, während ich darauf warte, dass die letzten Threads fertig sind. Um es schneller zu machen, habe ich die Daten selbst in 64 Arbeitspakete unterteilt und Aufgaben dafür erstellt. (Dann Task.WaitAll, um auf den Abschluss zu warten.) Die Idee ist, dass inaktive Threads ein Arbeitspaket abholen, um die Arbeit zu beenden, anstatt darauf zu warten, dass 1-2 Threads ihren (Parallel.For) zugewiesenen Block fertigstellen.
Tedd Hansen
1
Was macht Mehthod1()in diesem Beispiel?
Zapnologica
17

Parallel.ForEach optimiert (startet möglicherweise nicht einmal neue Threads) und blockiert, bis die Schleife beendet ist, und Task.Factory erstellt explizit eine neue Taskinstanz für jedes Element und kehrt zurück, bevor sie beendet sind (asynchrone Tasks). Parallel.Foreach ist viel effizienter.

Sogger
quelle
11

Meiner Ansicht nach ist das realistischste Szenario, wenn Aufgaben schwer zu erledigen sind. Shivprasads Ansatz konzentriert sich mehr auf die Objekterstellung / Speicherzuweisung als auf das Rechnen selbst. Ich habe eine Untersuchung durchgeführt, bei der die folgende Methode aufgerufen wurde:

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

Die Ausführung dieser Methode dauert ca. 0,5 Sekunden.

Ich habe es 200 Mal mit Parallel aufgerufen:

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

Dann habe ich es 200 Mal auf altmodische Weise genannt:

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

Der erste Fall wurde in 26656 ms abgeschlossen, der zweite in 24478 ms. Ich habe es viele Male wiederholt. Jedes Mal ist der zweite Ansatz geringfügig schneller.

user1089583
quelle
Die Verwendung von Parallel.For ist die altmodische Methode. Die Verwendung von Task wird für Arbeitseinheiten empfohlen, die nicht einheitlich sind. Microsoft MVPs und Designer der TPL erwähnen auch, dass die Verwendung von Aufgaben Threads effizienter nutzt und nicht so viele blockiert, während auf die Fertigstellung anderer Einheiten gewartet wird.
Suncat2000