Warum sollte ich einzelne "Warten auf Task.WhenAll" gegenüber mehreren Warten bevorzugen?

126

Sollte mir die Reihenfolge der Aufgabenerfüllung egal sein und ich brauche nur alle, um sie zu erledigen, sollte ich sie immer noch await Task.WhenAllanstelle von mehreren verwenden await? zB ist DoWork2unten eine bevorzugte Methode zu DoWork1(und warum?):

using System;
using System.Threading.Tasks;

namespace ConsoleApp
{
    class Program
    {
        static async Task<string> DoTaskAsync(string name, int timeout)
        {
            var start = DateTime.Now;
            Console.WriteLine("Enter {0}, {1}", name, timeout);
            await Task.Delay(timeout);
            Console.WriteLine("Exit {0}, {1}", name, (DateTime.Now - start).TotalMilliseconds);
            return name;
        }

        static async Task DoWork1()
        {
            var t1 = DoTaskAsync("t1.1", 3000);
            var t2 = DoTaskAsync("t1.2", 2000);
            var t3 = DoTaskAsync("t1.3", 1000);

            await t1; await t2; await t3;

            Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
        }

        static async Task DoWork2()
        {
            var t1 = DoTaskAsync("t2.1", 3000);
            var t2 = DoTaskAsync("t2.2", 2000);
            var t3 = DoTaskAsync("t2.3", 1000);

            await Task.WhenAll(t1, t2, t3);

            Console.WriteLine("DoWork2 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
        }


        static void Main(string[] args)
        {
            Task.WhenAll(DoWork1(), DoWork2()).Wait();
        }
    }
}
avo
quelle
2
Was ist, wenn Sie nicht wissen, wie viele Aufgaben Sie parallel ausführen müssen? Was ist, wenn 1000 Aufgaben ausgeführt werden müssen? Der erste wird nicht gut lesbar sein await t1; await t2; ....; await tn=> der zweite ist in beiden Fällen immer die beste Wahl
cuongle
Ihr Kommentar macht Sinn. Ich habe nur versucht, etwas für mich selbst zu klären, was mit einer anderen Frage zusammenhängt, die ich kürzlich beantwortet habe . In diesem Fall gab es 3 Aufgaben.
Avo

Antworten:

112

Ja, verwenden WhenAllSie , weil alle Fehler gleichzeitig weitergegeben werden. Mit den mehreren Wartezeiten verlieren Sie Fehler, wenn einer der früheren Würfe wartet.

Ein weiterer wichtiger Unterschied besteht darin, dass darauf WhenAllgewartet wird, dass alle Aufgaben auch bei Fehlern (fehlerhafte oder abgebrochene Aufgaben) abgeschlossen sind. Manuelles Warten nacheinander würde zu unerwarteter Parallelität führen, da der Teil Ihres Programms, der warten möchte, tatsächlich vorzeitig fortgesetzt wird.

Ich denke, es erleichtert auch das Lesen des Codes, da die gewünschte Semantik direkt im Code dokumentiert ist.

usr
quelle
9
"Weil es alle Fehler auf einmal verbreitet" Nicht, wenn Sie awaitdas Ergebnis.
Svick
2
Was die Frage betrifft, wie Ausnahmen mit Task verwaltet werden, gibt dieser Artikel einen schnellen, aber guten Einblick in die Gründe dafür (und zufällig werden auch die Vorteile von WhenAll im Gegensatz zu mehreren Wartezeiten vorübergehend erwähnt): Blogs .msdn.com / b / pfxteam / archive / 2011/09/28/10217876.aspx
Oskar Lindberg
5
@OskarLindberg Das OP startet alle Aufgaben, bevor er auf die erste wartet. Sie laufen also gleichzeitig. Danke für den Link.
usr
3
@usr Ich war immer noch neugierig zu wissen, ob WhenAll keine cleveren Dinge wie das Beibehalten des gleichen SynchronizationContext tut, um seine Vorteile weiter von der Semantik abzuwenden. Ich habe keine schlüssige Dokumentation gefunden, aber mit Blick auf die IL sind offensichtlich unterschiedliche Implementierungen von IAsyncStateMachine im Spiel. Ich lese IL nicht so gut, aber WhenAll scheint zumindest effizienteren IL-Code zu generieren. (In jedem Fall ist die Tatsache allein, dass das Ergebnis von WhenAll den Status aller für mich beteiligten Aufgaben widerspiegelt, Grund genug, es in den meisten Fällen zu bevorzugen.)
Oskar Lindberg
17
Ein weiterer wichtiger Unterschied besteht darin, dass WhenAll darauf wartet, dass alle Aufgaben abgeschlossen sind, auch wenn z. B. t1 oder t2 eine Ausnahme auslösen oder abgebrochen werden.
Magnus
28

Mein Verständnis ist, dass der Hauptgrund Task.WhenAll, mehrere awaits vorzuziehen , die Leistung / Aufgabe "churning" ist: Die DoWork1Methode macht ungefähr so:

  • Beginnen Sie mit einem bestimmten Kontext
  • Speichern Sie den Kontext
  • warte auf t1
  • Stellen Sie den ursprünglichen Kontext wieder her
  • Speichern Sie den Kontext
  • warte auf t2
  • Stellen Sie den ursprünglichen Kontext wieder her
  • Speichern Sie den Kontext
  • warte auf t3
  • Stellen Sie den ursprünglichen Kontext wieder her

Im Gegensatz DoWork2dazu:

  • Beginnen Sie mit einem bestimmten Kontext
  • Speichern Sie den Kontext
  • Warten Sie auf alle t1, t2 und t3
  • Stellen Sie den ursprünglichen Kontext wieder her

Ob dies für Ihren speziellen Fall groß genug ist, ist natürlich "kontextabhängig" (entschuldigen Sie das Wortspiel).

Marcel Popescu
quelle
4
Sie scheinen zu denken, dass das Senden einer Nachricht an den Synchronisationskontext teuer ist. Es ist wirklich nicht. Sie haben einen Delegaten, der einer Warteschlange hinzugefügt wird. Diese Warteschlange wird gelesen und der Delegat ausgeführt. Der damit verbundene Aufwand ist ehrlich gesagt sehr gering. Es ist nicht nichts, aber es ist auch nicht groß. Die Kosten für asynchrone Operationen werden diesen Aufwand in fast allen Fällen in den Schatten stellen.
Servy
Einverstanden war es nur der einzige Grund, warum ich mir vorstellen konnte, einen dem anderen vorzuziehen. Nun, das plus die Ähnlichkeit mit Task.WaitAll, wo das Wechseln von Threads höhere Kosten verursacht.
Marcel Popescu
1
@Servy Wie Marcel darauf hinweist, dass WIRKLICH davon abhängt. Wenn Sie beispielsweise "Warten" grundsätzlich für alle Datenbankaufgaben verwenden und diese Datenbank sich auf demselben Computer wie die asp.net-Instanz befindet, gibt es Fälle, in denen Sie auf einen Datenbank-Treffer warten, der im Speicher im Index gespeichert ist und billiger ist als dieser Synchronisationsschalter und Threadpool Shuffle. In solchen Szenarien könnte es mit WhenAll () einen signifikanten Gesamtsieg geben, also ... es kommt wirklich darauf an.
Chris Moschini
3
@ChrisMoschini Es gibt keine Möglichkeit, dass die DB-Abfrage, selbst wenn sie eine DB trifft, die sich auf demselben Computer wie der Server befindet, schneller ist als der Aufwand für das Hinzufügen einiger Delegaten zu einer Nachrichtenpumpe. Diese In-Memory-Abfrage wird mit ziemlicher Sicherheit noch viel langsamer sein.
Servy
Beachten Sie auch, dass wenn t1 langsamer und t2 und t3 schneller sind, der andere sofort auf die Rückkehr wartet.
David Refaeli
18

Eine asynchrone Methode ist als Zustandsmaschine implementiert. Es ist möglich, Methoden so zu schreiben, dass sie nicht in Zustandsmaschinen kompiliert werden. Dies wird häufig als schnelle asynchrone Methode bezeichnet. Diese können folgendermaßen implementiert werden:

public Task DoSomethingAsync()
{
    return DoSomethingElseAsync();
}

Bei Verwendung ist Task.WhenAlles möglich, diesen Fast-Track-Code beizubehalten und gleichzeitig sicherzustellen, dass der Anrufer warten kann, bis alle Aufgaben erledigt sind, z.

public Task DoSomethingAsync()
{
    var t1 = DoTaskAsync("t2.1", 3000);
    var t2 = DoTaskAsync("t2.2", 2000);
    var t3 = DoTaskAsync("t2.3", 1000);

    return Task.WhenAll(t1, t2, t3);
}
Lukazoid
quelle
7

(Haftungsausschluss: Diese Antwort stammt aus Ian Griffiths TPL Async-Kurs zu Pluralsight )

Ein weiterer Grund, WhenAll zu bevorzugen, ist die Ausnahmebehandlung.

Angenommen, Sie hatten einen Try-Catch-Block für Ihre DoWork-Methoden und nehmen an, dass sie verschiedene DoTask-Methoden aufrufen:

static async Task DoWork1() // modified with try-catch
{
    try
    {
        var t1 = DoTask1Async("t1.1", 3000);
        var t2 = DoTask2Async("t1.2", 2000);
        var t3 = DoTask3Async("t1.3", 1000);

        await t1; await t2; await t3;

        Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
    }
    catch (Exception x)
    {
        // ...
    }

}

In diesem Fall wird nur die erste abgefangen, wenn alle drei Aufgaben Ausnahmen auslösen. Jede spätere Ausnahme geht verloren. Dh wenn t2 und t3 eine Ausnahme auslösen, wird nur t2 abgefangen; usw. Die Ausnahmen für nachfolgende Aufgaben bleiben unbemerkt.

Wo wie in WhenAll - Wenn eine oder alle Aufgaben fehlerhaft sind, enthält die resultierende Aufgabe alle Ausnahmen. Das Schlüsselwort await löst immer noch die erste Ausnahme aus. Die anderen Ausnahmen bleiben also praktisch unbemerkt. Eine Möglichkeit, dies zu überwinden, besteht darin, nach der Aufgabe WhenAll eine leere Fortsetzung hinzuzufügen und das Warten dort abzulegen. Auf diese Weise löst die Ergebniseigenschaft die vollständige Aggregatausnahme aus, wenn die Aufgabe fehlschlägt:

static async Task DoWork2() //modified to catch all exceptions
{
    try
    {
        var t1 = DoTask1Async("t1.1", 3000);
        var t2 = DoTask2Async("t1.2", 2000);
        var t3 = DoTask3Async("t1.3", 1000);

        var t = Task.WhenAll(t1, t2, t3);
        await t.ContinueWith(x => { });

        Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t.Result[0], t.Result[1], t.Result[2]));
    }
    catch (Exception x)
    {
        // ...
    }
}
David Refaeli
quelle
6

Die anderen Antworten auf diese Frage bieten technische Gründe, warum await Task.WhenAll(t1, t2, t3);bevorzugt wird. Diese Antwort zielt darauf ab, sie von einer weicheren Seite zu betrachten (auf die @usr anspielt), während sie immer noch zu dem gleichen Schluss kommt.

await Task.WhenAll(t1, t2, t3); ist ein funktionalerer Ansatz, da er die Absicht erklärt und atomar ist.

Mit await t1; await t2; await t3;, es verhindert , dass nichts einen Mitspieler (oder vielleicht sogar Ihre Zukunft selbst!) Vom Hinzufügen von Code zwischen dem einzelnenawait Aussagen. Sicher, Sie haben es auf eine Zeile komprimiert, um dies im Wesentlichen zu erreichen, aber das löst das Problem nicht. Außerdem ist es in einer Teameinstellung im Allgemeinen eine schlechte Form, mehrere Anweisungen in eine bestimmte Codezeile aufzunehmen, da dies das Scannen der Quelldatei für das menschliche Auge erschweren kann.

Einfach ausgedrückt, await Task.WhenAll(t1, t2, t3);ist wartbarer, da es Ihre Absicht klarer kommuniziert und weniger anfällig für besondere Fehler ist, die durch wohlmeinende Aktualisierungen des Codes oder sogar durch fehlerhafte Zusammenführungen entstehen können.

rarrarrarrr
quelle