Warten auf mehrere Aufgaben mit unterschiedlichen Ergebnissen

237

Ich habe 3 Aufgaben:

private async Task<Cat> FeedCat() {}
private async Task<House> SellHouse() {}
private async Task<Tesla> BuyCar() {}

Sie müssen alle ausgeführt werden, bevor mein Code fortgesetzt werden kann, und ich benötige auch die Ergebnisse von jedem. Keines der Ergebnisse hat etwas miteinander gemeinsam

Wie rufe ich an und warte, bis die 3 Aufgaben abgeschlossen sind, und erhalte dann die Ergebnisse?

Ian Vink
quelle
25
Haben Sie Bestellanforderungen? Das heißt, möchten Sie das Haus erst verkaufen, nachdem die Katze gefüttert wurde?
Eric Lippert

Antworten:

411

Nach der Verwendung WhenAllkönnen Sie die Ergebnisse einzeln herausziehen mit await:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

Sie können auch verwenden Task.Result(da Sie zu diesem Zeitpunkt wissen, dass alle erfolgreich abgeschlossen wurden). Ich empfehle jedoch die Verwendung, awaitda dies eindeutig korrekt ist und Resultin anderen Szenarien Probleme verursachen kann.

Stephen Cleary
quelle
83
Sie können das einfach ganz daraus entfernen WhenAll; Durch die Wartezeiten wird sichergestellt, dass Sie die drei späteren Aufgaben erst dann erledigen, wenn alle Aufgaben erledigt sind.
Servy
134
Task.WhenAll()ermöglicht das Ausführen der Aufgabe im parallelen Modus. Ich kann nicht verstehen, warum @Servy vorgeschlagen hat, es zu entfernen. Ohne die werden WhenAllsie einzeln ausgeführt
Sergey G.
86
@ Sergey: Die Aufgaben werden sofort ausgeführt. ZB catTaskläuft bereits, wenn es zurückkommt FeedCat. Beide Ansätze funktionieren also - die einzige Frage ist, ob Sie sie einzeln awaitoder alle zusammen verwenden möchten . Die Fehlerbehandlung unterscheidet sich geringfügig - wenn Sie sie verwenden Task.WhenAll, werden awaitsie alle verwendet, auch wenn einer von ihnen vorzeitig ausfällt.
Stephen Cleary
23
@ Sergey Calling WhenAllhat keinen Einfluss darauf, wann oder wie die Vorgänge ausgeführt werden. Es besteht nur die Möglichkeit, die Beobachtung der Ergebnisse zu beeinflussen. In diesem speziellen Fall besteht der einzige Unterschied darin, dass ein Fehler in einer der ersten beiden Methoden dazu führen würde, dass die Ausnahme in meinem Aufrufstapel früher in meiner Methode als in Stephens ausgelöst wird (obwohl immer derselbe Fehler ausgelöst wird, falls vorhanden ).
Servy
36
@ Sergey: Der Schlüssel ist, dass asynchrone Methoden immer "heiße" (bereits gestartete) Aufgaben zurückgeben.
Stephen Cleary
99

Nur awaitdie drei Aufgaben getrennt, nachdem Sie alle gestartet haben.

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

var cat = await catTask;
var house = await houseTask;
var car = await carTask;
Servieren
quelle
8
@Bargitta Nein, das ist falsch. Sie werden ihre Arbeit parallel erledigen. Fühlen Sie sich frei, es auszuführen und überzeugen Sie sich selbst.
Servy
5
Die Leute stellen nach Jahren immer wieder die gleiche Frage ... Ich halte es für wichtig, noch einmal zu betonen, dass eine Aufgabe im Hauptteil der Antwort " anfängt zu erstellen " : Vielleicht lesen sie keine Kommentare
9
@StephenYork Das Hinzufügen von Task.WhenAllÄnderungen ändert buchstäblich nichts am Verhalten des Programms in irgendeiner beobachtbaren Weise. Es ist ein rein redundanter Methodenaufruf. Sie können es gerne als ästhetische Wahl hinzufügen, wenn Sie möchten, aber es ändert nichts an der Funktionsweise des Codes. Die Ausführungszeit des Codes ist mit oder ohne diesen Methodenaufruf identisch (technisch gesehen ist der Aufwand für den Aufruf sehr geringWhenAll , sollte aber vernachlässigbar sein), sodass diese Version nur geringfügig länger ausgeführt werden kann als diese Version.
Servy
4
@StephenYork In Ihrem Beispiel werden die Vorgänge aus zwei Gründen nacheinander ausgeführt. Ihre asynchronen Methoden sind nicht wirklich asynchron, sondern synchron. Die Tatsache, dass Sie über synchrone Methoden verfügen, die immer bereits abgeschlossene Aufgaben zurückgeben, verhindert, dass diese gleichzeitig ausgeführt werden. Als Nächstes tun Sie nicht das, was in dieser Antwort gezeigt wird, indem Sie alle drei asynchronen Methoden starten und dann nacheinander auf die drei Aufgaben warten. In Ihrem Beispiel wird nicht jede Methode aufgerufen, bis die vorherige beendet ist, sodass im Gegensatz zu diesem Code explizit verhindert wird, dass eine Methode gestartet wird, bis die vorherige beendet ist.
Servy
4
@MarcvanNieuwenhuijzen Das ist nachweislich nicht wahr, wie in den Kommentaren hier und bei anderen Antworten diskutiert wurde. Hinzufügen WhenAllist eine rein ästhetische Veränderung. Der einzige beobachtbare Unterschied im Verhalten besteht darin, ob Sie auf den Abschluss späterer Aufgaben warten, wenn eine frühere Aufgabe fehlerhaft ist, was normalerweise nicht erforderlich ist. Wenn Sie den zahlreichen Erklärungen nicht glauben, warum Ihre Aussage nicht wahr ist, können Sie den Code einfach selbst ausführen und feststellen, dass er nicht wahr ist.
Servy
37

Wenn Sie C # 7 verwenden, können Sie eine praktische Wrapper-Methode wie diese verwenden ...

public static class TaskEx
{
    public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
    {
        return (await task1, await task2);
    }
}

... um eine bequeme Syntax wie diese zu aktivieren, wenn Sie auf mehrere Aufgaben mit unterschiedlichen Rückgabetypen warten möchten. Sie müssten natürlich mehrere Überladungen vornehmen, damit eine unterschiedliche Anzahl von Aufgaben auf Sie wartet.

var (someInt, someString) = await TaskEx.WhenAll(GetIntAsync(), GetStringAsync());

In der Antwort von Marc Gravell finden Sie jedoch einige Optimierungen in Bezug auf ValueTask und bereits abgeschlossene Aufgaben, wenn Sie dieses Beispiel in etwas Reales verwandeln möchten.

Joel Mueller
quelle
Tupel sind die einzige C # 7-Funktion, die hier beteiligt ist. Die sind definitiv in der endgültigen Version.
Joel Mueller
Ich kenne Tupel und c # 7. Ich meine, ich kann die Methode WhenAll nicht finden, die Tupel zurückgibt. Welcher Namespace / welches Paket?
Yury Scherbakov
@YuryShcherbakov Task.WhenAll()gibt kein Tupel zurück. Eine wird aus den ResultEigenschaften der bereitgestellten Aufgaben erstellt, nachdem die von zurückgegebene Aufgabe Task.WhenAll()abgeschlossen ist.
Chris Charabaruk
2
Ich würde vorschlagen, die .ResultAnrufe gemäß Stephens Argumentation zu ersetzen , um zu vermeiden, dass andere die schlechte Praxis fortsetzen, indem Sie Ihr Beispiel kopieren.
Julealgon
Ich frage mich, warum diese Methode nicht Teil des Frameworks ist. Es scheint so nützlich. Haben sie keine Zeit mehr und mussten bei einem einzigen Rückgabetyp anhalten?
Ian Grainger
14

Gegeben drei Aufgaben - FeedCat(), SellHouse()und BuyCar()gibt es zwei interessante Fälle: Entweder sie alle vollständig synchron (aus irgendeinem Grund, vielleicht das Caching oder ein Fehler), oder sie es nicht tun.

Nehmen wir an, wir haben aus der Frage:

Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();
    // what here?
}

Ein einfacher Ansatz wäre nun:

Task.WhenAll(x, y, z);

aber ... das ist nicht bequem für die Verarbeitung der Ergebnisse; Das möchten wir normalerweise await:

async Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    await Task.WhenAll(x, y, z);
    // presumably we want to do something with the results...
    return DoWhatever(x.Result, y.Result, z.Result);
}

Dies verursacht jedoch viel Overhead und weist verschiedene Arrays (einschließlich des params Task[]Arrays) und Listen (intern) zu. Es funktioniert, aber es ist nicht großartig, IMO. In vielerlei Hinsicht ist es einfacher , eine asyncOperation zu verwenden, und zwar awaitjeweils nacheinander:

async Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    // do something with the results...
    return DoWhatever(await x, await y, await z);
}

Im Gegensatz zu einigen der obigen Kommentare macht die Verwendung von awaitanstelle von keinen Unterschied für die Ausführung der Aufgaben (gleichzeitig, nacheinander usw.). Auf höchster Ebene vor einer guten Compiler-Unterstützung für / und war nützlich, wenn diese Dinge nicht existierten . Dies ist auch nützlich, wenn Sie eine beliebige Reihe von Aufgaben anstelle von drei diskreten Aufgaben haben.Task.WhenAllTask.WhenAll asyncawait

Aber: Wir haben immer noch das Problem, dass async/ awaitviel Compiler-Rauschen für die Fortsetzung erzeugt. Ist es wahrscheinlich , dass die Aufgaben ist vielleicht tatsächlich synchron abgeschlossen hat , dann können wir diese optimieren , indem sie mit einem asynchronen Rückfall in einem synchronen Pfad Aufbau:

Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    if(x.Status == TaskStatus.RanToCompletion &&
       y.Status == TaskStatus.RanToCompletion &&
       z.Status == TaskStatus.RanToCompletion)
        return Task.FromResult(
          DoWhatever(a.Result, b.Result, c.Result));
       // we can safely access .Result, as they are known
       // to be ran-to-completion

    return Awaited(x, y, z);
}

async Task Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
    return DoWhatever(await x, await y, await z);
}

Dieser Ansatz "Synchronisierungspfad mit asynchronem Fallback" wird immer häufiger verwendet, insbesondere bei Hochleistungscode, bei dem synchrone Abschlüsse relativ häufig sind. Beachten Sie, dass es überhaupt nicht hilft, wenn die Fertigstellung immer wirklich asynchron ist.

Zusätzliche Dinge, die hier gelten:

  1. Mit dem aktuellen C # wird ein allgemeines Muster für die asyncFallback-Methode verwendet, die üblicherweise als lokale Funktion implementiert wird:

    Task<string> DoTheThings() {
        async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
            return DoWhatever(await a, await b, await c);
        }
        Task<Cat> x = FeedCat();
        Task<House> y = SellHouse();
        Task<Tesla> z = BuyCar();
    
        if(x.Status == TaskStatus.RanToCompletion &&
           y.Status == TaskStatus.RanToCompletion &&
           z.Status == TaskStatus.RanToCompletion)
            return Task.FromResult(
              DoWhatever(a.Result, b.Result, c.Result));
           // we can safely access .Result, as they are known
           // to be ran-to-completion
    
        return Awaited(x, y, z);
    }
  2. lieber ValueTask<T>, Task<T>wenn es eine gute Chance gibt, dass die Dinge jemals vollständig synchron mit vielen verschiedenen Rückgabewerten sind:

    ValueTask<string> DoTheThings() {
        async ValueTask<string> Awaited(ValueTask<Cat> a, Task<House> b, Task<Tesla> c) {
            return DoWhatever(await a, await b, await c);
        }
        ValueTask<Cat> x = FeedCat();
        ValueTask<House> y = SellHouse();
        ValueTask<Tesla> z = BuyCar();
    
        if(x.IsCompletedSuccessfully &&
           y.IsCompletedSuccessfully &&
           z.IsCompletedSuccessfully)
            return new ValueTask<string>(
              DoWhatever(a.Result, b.Result, c.Result));
           // we can safely access .Result, as they are known
           // to be ran-to-completion
    
        return Awaited(x, y, z);
    }
  3. wenn möglich, lieber IsCompletedSuccessfullyzu Status == TaskStatus.RanToCompletion; Dies gibt es jetzt in .NET Core für Taskund überall fürValueTask<T>

Marc Gravell
quelle
"Im Gegensatz zu verschiedenen Antworten hier hat die Verwendung von" Warten "anstelle von" Task.WhenAll "keinen Einfluss darauf, wie die Aufgaben ausgeführt werden (gleichzeitig, nacheinander usw.)." Ich sehe keine Antwort, die dies aussagt. Ich hätte sie bereits kommentiert und so viel gesagt, wenn sie es getan hätten. Es gibt viele Kommentare zu vielen Antworten, die das sagen, aber keine Antworten. Auf welche beziehen Sie sich? Beachten Sie auch, dass Ihre Antwort nicht das Ergebnis der Aufgaben behandelt (oder die Tatsache berücksichtigt, dass die Ergebnisse alle von einem anderen Typ sind). Sie haben sie in einer Methode zusammengestellt, die nur dann a zurückgibt, Taskwenn sie alle fertig sind, ohne die Ergebnisse zu verwenden.
Servy
@Servy du hast recht, das waren Kommentare; Ich werde eine
Optimierung
@Servy Tweak hinzugefügt
Marc Gravell
Auch wenn Sie die Übergabe synchroner Aufgaben frühzeitig beenden möchten, können Sie auch Aufgaben synchron bearbeiten, die abgebrochen oder fehlerhaft sind, anstatt nur die erfolgreich abgeschlossenen. Wenn Sie die Entscheidung getroffen haben, dass es sich um eine Optimierung handelt, die Ihr Programm benötigt (was selten sein wird, aber passieren wird), können Sie genauso gut den ganzen Weg gehen.
Servy
@Servy ist ein komplexes Thema - Sie erhalten unterschiedliche Ausnahmesemantiken aus den beiden Szenarien - das Warten auf das Auslösen einer Ausnahme verhält sich anders als der Zugriff auf .Result zum Auslösen der Ausnahme. IMO an diesem Punkt sollten wir awaitdie "bessere" Ausnahmesemantik erhalten, unter der Annahme, dass Ausnahmen selten, aber sinnvoll sind
Marc Gravell
12

Sie können sie in Aufgaben speichern und dann auf alle warten:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

Cat cat = await catTask;
House house = await houseTask;
Car car = await carTask;
Reed Copsey
quelle
führt var catTask = FeedCat()die Funktion nicht aus FeedCat()und speichert das Ergebnis catTask, um das await Task.WhenAll()Teil unbrauchbar zu machen, da die Methode bereits ausgeführt wurde?
Kraang Prime
1
@sanuel wenn sie Aufgabe <t> zurückgeben, dann nein ... sie starten das asynchrone Öffnen, aber warten nicht darauf
Reed Copsey
Ich denke nicht, dass dies korrekt ist. Bitte lesen Sie die Diskussionen unter @ StephenClearys Antwort. Siehe auch Servys Antwort.
Rosdi Kasim
1
wenn ich .ConfigrtueAwait (false) hinzufügen muss. Würde ich es nur zu Task.WhenAll oder zu jedem folgenden Kellner hinzufügen?
AstroSharp
@AstroSharp im Allgemeinen ist es eine gute Idee, es allen hinzuzufügen (wenn das erste abgeschlossen ist, wird es effektiv ignoriert), aber in diesem Fall wäre es wahrscheinlich in Ordnung, nur das erste zu tun - es sei denn, es gibt mehr Asynchronität Sachen, die später passieren.
Reed Copsey
6

Wenn Sie versuchen, alle Fehler zu protokollieren, stellen Sie sicher, dass Sie die Zeile Task.WhenAll in Ihrem Code beibehalten. Viele Kommentare deuten darauf hin, dass Sie sie entfernen und auf einzelne Aufgaben warten können. Task.WhenAll ist wirklich wichtig für die Fehlerbehandlung. Ohne diese Zeile lassen Sie Ihren Code möglicherweise für unbeobachtete Ausnahmen offen.

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

Stellen Sie sich vor, FeedCat löst eine Ausnahme im folgenden Code aus:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

In diesem Fall werden Sie weder auf houseTask noch auf carTask warten. Hier gibt es 3 mögliche Szenarien:

  1. SellHouse wurde bereits erfolgreich abgeschlossen, als FeedCat fehlschlug. In diesem Fall geht es dir gut.

  2. SellHouse ist nicht vollständig und schlägt mit Ausnahme irgendwann fehl. Eine Ausnahme wird nicht beachtet und im Finalizer-Thread erneut ausgelöst.

  3. SellHouse ist nicht vollständig und enthält Wartezeiten. Falls Ihr Code in ASP.NET ausgeführt wird, schlägt SellHouse fehl, sobald einige der darin enthaltenen Wartezeiten abgeschlossen sind. Dies geschieht, weil Sie im Grunde genommen einen Fire & Forget-Anruf getätigt haben und der Synchronisationskontext verloren gegangen ist, sobald FeedCat fehlgeschlagen ist.

Hier ist ein Fehler, den Sie für Fall (3) erhalten:

System.AggregateException: A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread. ---> System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   at System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
   at System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
   at System.Threading.Tasks.Task.Execute()
   --- End of inner exception stack trace ---
---> (Inner Exception #0) System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   at System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
   at System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
   at System.Threading.Tasks.Task.Execute()<---

In Fall (2) wird ein ähnlicher Fehler angezeigt, jedoch mit dem ursprünglichen Ausnahmestapel-Trace.

Für .NET 4.0 und höher können Sie nicht beobachtete Ausnahmen mit TaskScheduler.UnobservedTaskException abfangen. Für .NET 4.5 und höher werden nicht beobachtete Ausnahmen standardmäßig verschluckt. Für .NET 4.0 stürzt eine nicht beobachtete Ausnahme Ihren Prozess ab.

Weitere Details finden Sie hier: Behandlung von Aufgabenausnahmen in .NET 4.5

samfromlv
quelle
2

Sie können Task.WhenAllwie erwähnt oder verwenden Task.WaitAll, je nachdem, ob der Thread warten soll. Schauen Sie sich den Link an, um eine Erklärung für beide zu erhalten.

WaitAll vs WhenAll

christiandev
quelle
2

Verwenden Sie Task.WhenAllund warten Sie dann auf die Ergebnisse:

var tCat = FeedCat();
var tHouse = SellHouse();
var tCar = BuyCar();
await Task.WhenAll(tCat, tHouse, tCar);
Cat cat = await tCat;
House house = await tHouse;
Tesla car = await tCar; 
//as they have all definitely finished, you could also use Task.Value.
Es ist nicht wahr.
quelle
mm ... nicht Task.Value (vielleicht existierte es 2013?), sondern tCat.Result, tHouse.Result oder tCar.Result
Stephen York
1

Vorwärtswarnung

Nur ein kurzer Überblick für diejenigen, die diesen und ähnliche Threads besuchen und nach einer Möglichkeit suchen, EntityFramework mithilfe des Tool-Sets async + await + task zu parallelisieren : Das hier gezeigte Muster ist solide, wenn es jedoch um die spezielle Schneeflocke von EF geht, werden Sie es nicht tun Erzielen Sie eine parallele Ausführung, es sei denn, Sie verwenden eine separate (neue) Datenbankkontextinstanz in jedem beteiligten * Async () -Aufruf.

Diese Art von Dingen ist aufgrund inhärenter Designbeschränkungen von ef-db-Kontexten erforderlich, die es verbieten, mehrere Abfragen parallel in derselben ef-db-Kontextinstanz auszuführen.


Wenn Sie die bereits gegebenen Antworten nutzen, können Sie auf diese Weise sicherstellen, dass Sie alle Werte erfassen, auch wenn eine oder mehrere der Aufgaben zu einer Ausnahme führen:

  public async Task<string> Foobar() {
    async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
        return DoSomething(await a, await b, await c);
    }

    using (var carTask = BuyCarAsync())
    using (var catTask = FeedCatAsync())
    using (var houseTask = SellHouseAsync())
    {
        if (carTask.Status == TaskStatus.RanToCompletion //triple
            && catTask.Status == TaskStatus.RanToCompletion //cache
            && houseTask.Status == TaskStatus.RanToCompletion) { //hits
            return Task.FromResult(DoSomething(catTask.Result, carTask.Result, houseTask.Result)); //fast-track
        }

        cat = await catTask;
        car = await carTask;
        house = await houseTask;
        //or Task.AwaitAll(carTask, catTask, houseTask);
        //or await Task.WhenAll(carTask, catTask, houseTask);
        //it depends on how you like exception handling better

        return Awaited(catTask, carTask, houseTask);
   }
 }

Eine alternative Implementierung mit mehr oder weniger denselben Leistungsmerkmalen könnte sein:

 public async Task<string> Foobar() {
    using (var carTask = BuyCarAsync())
    using (var catTask = FeedCatAsync())
    using (var houseTask = SellHouseAsync())
    {
        cat = catTask.Status == TaskStatus.RanToCompletion ? catTask.Result : (await catTask);
        car = carTask.Status == TaskStatus.RanToCompletion ? carTask.Result : (await carTask);
        house = houseTask.Status == TaskStatus.RanToCompletion ? houseTask.Result : (await houseTask);

        return DoSomething(cat, car, house);
     }
 }
XDS
quelle
-1
var dn = await Task.WhenAll<dynamic>(FeedCat(),SellHouse(),BuyCar());

Wenn Sie auf Cat zugreifen möchten, gehen Sie folgendermaßen vor:

var ct = (Cat)dn[0];

Dies ist sehr einfach und sehr nützlich, es besteht keine Notwendigkeit, eine komplexe Lösung zu suchen.

Stier
quelle
1
Es gibt nur ein Problem: dynamicist der Teufel. Es ist für kniffliges COM-Interop und dergleichen gedacht und sollte nicht in Situationen verwendet werden, in denen es nicht unbedingt benötigt wird. Besonders wenn Sie Wert auf Leistung legen. Oder geben Sie Sicherheit ein. Oder Refactoring. Oder das Debuggen.
Joel Mueller