Verwendung von Task.WaitAll vs. Task.WhenAll in .NET

Die TPL (Task Parallel Library) ist eine der interessantesten neuen Funktionen, die in den neuesten Versionen von .NET Framework hinzugefügt wurden. Die Methoden Task.WaitAll und Task.WhenAll sind zwei wichtige und häufig verwendete Methoden in der TPL.

Task.WaitAll blockiert den aktuellen Thread, bis alle anderen Aufgaben ausgeführt wurden. Mit der Task.WhenAll-Methode wird eine Aufgabe erstellt, die nur dann ausgeführt wird, wenn alle anderen Aufgaben abgeschlossen sind.

Wenn Sie also Task.WhenAll verwenden, erhalten Sie ein Task-Objekt, das nicht vollständig ist. Es wird jedoch nicht blockiert, sondern ermöglicht die Ausführung des Programms. Im Gegenteil, der Task.WaitAll-Methodenaufruf blockiert und wartet, bis alle anderen Aufgaben abgeschlossen sind.

Im Wesentlichen gibt Task.WhenAll eine Aufgabe aus, die nicht abgeschlossen ist. Sie können jedoch ContinueWith verwenden, sobald die angegebenen Aufgaben ausgeführt wurden. Beachten Sie, dass weder Task.WhenAll noch Task.WaitAll die Aufgaben tatsächlich ausführen. Das heißt, mit diesen Methoden werden keine Aufgaben gestartet. So wird ContinueWith mit Task.WhenAll verwendet: 

Task.WhenAll (taskList) .ContinueWith (t => {

  // schreibe deinen Code hier

});

In der Dokumentation von Microsoft heißt es, dass Task.WhenAll "eine Aufgabe erstellt, die abgeschlossen wird, wenn alle Aufgabenobjekte in einer Aufzählungssammlung abgeschlossen sind."

Task.WhenAll vs. Task.WaitAll

Lassen Sie mich den Unterschied zwischen diesen beiden Methoden anhand eines einfachen Beispiels erklären. Angenommen, Sie haben eine Aufgabe, die eine Aktivität mit dem UI-Thread ausführt. Angenommen, einige Animationen müssen in der Benutzeroberfläche angezeigt werden. Wenn Sie jetzt Task.WaitAll verwenden, wird die Benutzeroberfläche blockiert und erst aktualisiert, wenn alle zugehörigen Aufgaben abgeschlossen und die Blockierung freigegeben sind. Wenn Sie jedoch Task.WhenAll in derselben Anwendung verwenden, wird der UI-Thread nicht blockiert und wie gewohnt aktualisiert.

Welche dieser Methoden sollten Sie wann anwenden? Nun, Sie können WaitAll verwenden, wenn die Absicht synchron blockiert wird, um die Ergebnisse zu erhalten. Wenn Sie jedoch die Asynchronität nutzen möchten, sollten Sie die WhenAll-Variante verwenden. Sie können Task.WhenAll abwarten, ohne den aktuellen Thread blockieren zu müssen. Daher möchten Sie möglicherweise wait mit Task.WhenAll in einer asynchronen Methode verwenden.

Während Task.WaitAll den aktuellen Thread blockiert, bis alle ausstehenden Aufgaben abgeschlossen sind, gibt Task.WhenAll ein Aufgabenobjekt zurück. Task.WaitAll löst eine AggregateException aus, wenn eine oder mehrere der Aufgaben eine Ausnahme auslösen. Wenn eine oder mehrere Aufgaben eine Ausnahme auslösen und Sie auf die Task.WhenAll-Methode warten, wird die AggregateException entpackt und nur die erste zurückgegeben.

Vermeiden Sie die Verwendung von Task.Run in Schleifen

Sie können Aufgaben verwenden, wenn Sie gleichzeitige Aktivitäten ausführen möchten. Wenn Sie ein hohes Maß an Parallelität benötigen, sind Aufgaben niemals eine gute Wahl. Es ist immer ratsam, die Verwendung von Thread-Pool-Threads in ASP.Net zu vermeiden. Daher sollten Sie Task.Run oder Task.factory.StartNew in ASP.Net nicht verwenden.

Task.Run sollte immer für CPU-gebundenen Code verwendet werden. Task.Run ist keine gute Wahl in ASP.Net-Anwendungen oder in Anwendungen, die die ASP.Net-Laufzeit nutzen, da die Arbeit nur in einen ThreadPool-Thread verlagert wird. Wenn Sie die ASP.Net-Web-API verwenden, verwendet die Anforderung bereits einen ThreadPool-Thread. Wenn Sie Task.Run in Ihrer ASP.Net-Web-API-Anwendung verwenden, beschränken Sie daher nur die Skalierbarkeit, indem Sie die Arbeit ohne Angabe von Gründen auf einen anderen Arbeitsthread verlagern.

Beachten Sie, dass die Verwendung von Task.Run in einer Schleife einen Nachteil hat. Wenn Sie die Task.Run-Methode in einer Schleife verwenden, werden mehrere Aufgaben erstellt - eine für jede Arbeitseinheit oder Iteration. Wenn Sie jedoch Parallel.ForEach anstelle von Task.Run in einer Schleife verwenden, wird ein Partitionierer erstellt, um zu vermeiden, dass mehr Aufgaben zum Ausführen der Aktivität erstellt werden, als benötigt werden. Dies kann die Leistung erheblich verbessern, da Sie zu viele Kontextwechsel vermeiden und dennoch mehrere Kerne in Ihrem System nutzen können.

Es ist zu beachten, dass Parallel.ForEach Partitioner intern verwendet, um die Sammlung in Arbeitselemente zu verteilen. Diese Verteilung erfolgt übrigens nicht für jede Aufgabe in der Liste der Elemente, sondern als Stapel. Dies verringert den Overhead und verbessert somit die Leistung. Mit anderen Worten, wenn Sie Task.Run oder Task.Factory.StartNew in einer Schleife verwenden, werden für jede Iteration in der Schleife explizit neue Aufgaben erstellt. Parallel.ForEach ist viel effizienter, da es die Ausführung optimiert, indem die Arbeitslast auf mehrere Kerne in Ihrem System verteilt wird.