387 Stimmen

In Erwartung mehrerer Aufgaben mit unterschiedlichen Ergebnissen

Ich habe 3 Aufgaben:

private async Task FüttereKatze() {}
private async Task VerkaufeHaus() {}
private async Task KaufeAuto() {}

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

Wie rufe ich die 3 Aufgaben auf, warte auf deren Abschluss und erhalte dann die Ergebnisse?

675voto

Stephen Cleary Punkte 402664

Nachdem Sie WhenAll verwendet haben, können Sie die Ergebnisse einzeln mit await abrufen:

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;

[Beachten Sie, dass asynchrone Methoden immer "heiße" (bereits gestartete) Tasks zurückgeben.]

Sie können auch Task.Result verwenden (da Sie zu diesem Zeitpunkt wissen, dass sie alle erfolgreich abgeschlossen wurden). Ich empfehle jedoch die Verwendung von await, da dies eindeutig korrekt ist, während Result in anderen Szenarien Probleme verursachen kann.

159voto

Servy Punkte 197305

Einfach die drei Aufgaben nacheinander awaiten, nachdem sie alle gestartet wurden:

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

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

Hinweis: Falls eine Ausnahme von einer der Aufgaben geworfen wird, wird dieser Code möglicherweise die Ausnahme zurückgeben, bevor spätere Aufgaben beendet sind, aber sie werden alle ausgeführt. In den meisten Situationen ist es wünschenswert, nicht zu warten, wenn man das Ergebnis bereits kennt. In Randfällen könnte es anders sein.

56voto

Joel Mueller Punkte 27700

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(Task task1, Task task2)
    {
        return (await task1, await task2);
    }
}

...um eine bequeme Syntax wie diese zu ermöglichen, wenn Sie auf mehrere Tasks mit unterschiedlichen Rückgabetypen warten möchten. Natürlich müssten Sie mehrere Überladungen für unterschiedliche Anzahlen von zu awaitenden Tasks erstellen.

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

Sehen Sie jedoch Marcs Gravells Antwort für einige Optimierungen rund um ValueTask und bereits abgeschlossene Tasks, wenn Sie beabsichtigen, dieses Beispiel in etwas Reales umzusetzen.

32voto

Marc Gravell Punkte 970173

Angesichts drei Aufgaben - FeedCat(), SellHouse() und BuyCar() gibt es zwei interessante Fälle: Entweder sie alle werden synchron abgeschlossen (aus irgendeinem Grund, vielleicht durch Caching oder einen Fehler), oder nicht.

Sagen wir, wir haben, ausgehend von der Frage:

Task DoTheThings() {
    Task x = FeedCat();
    Task y = SellHouse();
    Task z = BuyCar();
    // was hier?
}

Jetzt wäre ein einfacher Ansatz:

Task.WhenAll(x, y, z);

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

async Task DoTheThings() {
    Task x = FeedCat();
    Task y = SellHouse();
    Task z = BuyCar();

    await Task.WhenAll(x, y, z);
    // wahrscheinlich möchten wir etwas mit den Ergebnissen machen...
    return DoWhatever(x.Result, y.Result, z.Result);
}

aber das erzeugt viel Overhead und alloziert verschiedene Arrays (einschließlich des params Task[] Arrays) und Listen (intern). Es funktioniert, aber meiner Meinung nach ist es nicht optimal. In vielerlei Hinsicht ist es einfacher, eine async-Operation zu verwenden und einfach nacheinander auf jedes zu await:

async Task DoTheThings() {
    Task x = FeedCat();
    Task y = SellHouse();
    Task z = BuyCar();

    // etwas mit den Ergebnissen machen...
    return DoWhatever(await x, await y, await z);
}

Entgegen einiger der Kommentare oben hat es für die Ausführung der Aufgaben (parallel, sequentiell, etc.) keinen Unterschied, ob man await statt Task.WhenAll verwendet. Auf höchster Ebene vorvausgeht Task.WhenAll eine gute Compiler-Unterstützung für async/await und war nützlich, als diese Dinge nicht existierten. Es ist auch nützlich, wenn man ein beliebiges Array von Aufgaben hat, anstatt 3 diskrete Aufgaben.

Aber: Wir haben immer noch das Problem, dass async/await viel Compiler-Rauschen für die Fortsetzung erzeugt. Wenn es wahrscheinlich ist, dass die Aufgaben tatsächlich synchron abgeschlossen werden könnten, können wir dies optimieren, indem wir einen synchronen Pfad mit einem asynchronen Ausweichweg integrieren:

Task DoTheThings() {
    Task x = FeedCat();
    Task y = SellHouse();
    Task 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));
       // wir können sicher auf .Result zugreifen, da sie bekannt sind,
       // um vollständig ausgeführt zu sein

    return Awaited(x, y, z);
}

async Task Awaited(Task a, Task b, Task c) {
    return DoWhatever(await x, await y, await z);
}

Dieser "Synchroner Pfad mit asynchronem Ausweichweg"-Ansatz ist zunehmend verbreitet, insbesondere in High-Performance-Code, wo synchrone Abschlüsse relativ häufig sind. Beachten Sie, dass dies überhaupt nicht hilft, wenn die Fertigstellung immer wirklich asynchron ist.

Weitere Dinge, die hier gelten:

  1. bei aktuellem C# ist ein verbreitetes Muster für die async-Ausweichmethode häufig als lokale Funktion implementiert:

    Task DoTheThings() {
        async Task Awaited(Task a, Task b, Task c) {
            return DoWhatever(await a, await b, await c);
        }
        Task x = FeedCat();
        Task y = SellHouse();
        Task 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));
           // wir können sicher auf .Result zugreifen, da sie bekannt sind,
           // um vollständig ausgeführt zu sein
    
        return Awaited(x, y, z);
    }
  2. bevorzugen Sie ValueTask gegenüber Task, wenn die Wahrscheinlichkeit besteht, dass Dinge jemals vollständig synchron mit vielen unterschiedlichen Rückgabewerten abgeschlossen werden:

    ValueTask DoTheThings() {
        async ValueTask Awaited(ValueTask a, Task b, Task c) {
            return DoWhatever(await a, await b, await c);
        }
        ValueTask x = FeedCat();
        ValueTask y = SellHouse();
        ValueTask z = BuyCar();
    
        if(x.IsCompletedSuccessfully &&
           y.IsCompletedSuccessfully &&
           z.IsCompletedSuccessfully)
            return new ValueTask(
              DoWhatever(a.Result, b.Result, c.Result));
           // wir können sicher auf .Result zugreifen, da sie bekannt sind,
           // um vollständig ausgeführt zu sein
    
        return Awaited(x, y, z);
    }
  3. wenn möglich, bevorzugen Sie IsCompletedSuccessfully gegenüber Status == TaskStatus.RanToCompletion; das existiert jetzt in .NET Core für Task und überall für ValueTask

14voto

samfromlv Punkte 913

Wenn Sie versuchen, alle Fehler zu protokollieren, stellen Sie sicher, dass Sie die Zeile Task.WhenAll in Ihrem Code behalten, viele Kommentare legen nahe, dass Sie sie entfernen und auf einzelne Tasks warten können. Task.WhenAll ist wirklich wichtig für die Fehlerbehandlung. Ohne diese Zeile lassen Sie potenziell Ihren Code 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 wirft eine Ausnahme im folgenden Code:

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

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

In diesem Fall warten Sie niemals auf houseTask oder carTask. Hier gibt es 3 mögliche Szenarien:

  1. SellHouse ist bereits erfolgreich abgeschlossen, als FeedCat fehlschlägt. In diesem Fall ist alles in Ordnung.

  2. SellHouse ist nicht abgeschlossen und schlägt an einem Punkt mit einer Ausnahme fehl. Die Ausnahme wird nicht beobachtet und wird auf dem Finalisierungsthread erneut ausgelöst.

  3. SellHouse ist nicht abgeschlossen und enthält Awaits darin. Wenn Ihr Code in ASP.NET ausgeführt wird, schlägt SellHouse fehl, sobald eines der Awaits darin abgeschlossen ist. Dies geschieht, weil Sie im Grunde genommen einen Fire & Forget-Aufruf gemacht haben und der Synchronisierungskontext verloren gegangen ist, sobald FeedCat fehlgeschlagen ist.

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

System.AggregateException: Eine oder mehrere Ausnahmen von Tasks wurden nicht beobachtet, entweder indem auf den Task gewartet wurde oder auf seine Exception-Eigenschaft zugegriffen wurde. Als Folge wurde die unbeobachtete Ausnahme vom Finalisierungsthread erneut ausgelöst. ---> System.NullReferenceException: Der Objektverweis wurde nicht auf eine Objektinstanz festgelegt.
   bei System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   bei System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   bei System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
   bei System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
   bei System.Threading.Tasks.Task.Execute()
   --- Ende der internen Ausnahmestapelüberwachung ---
---> (Innere Ausnahme #0) System.NullReferenceException: Der Objektverweis wurde nicht auf eine Objektinstanz festgelegt.
   bei System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   bei System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   bei System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
   bei System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
   bei System.Threading.Tasks.Task.Execute()<---

Für Fall (2) erhalten Sie einen ähnlichen Fehler, jedoch mit dem ursprünglichen Ausnahme-Stack-Trace.

Ab .NET 4.0 können Sie unbeobachtete Ausnahmen mit TaskScheduler.UnobservedTaskException abfangen. Ab .NET 4.5 werden unbeobachtete Ausnahmen standardmäßig verschluckt, für .NET 4.0 wird eine unbeobachtete Ausnahme Ihren Prozess abstürzen lassen.

Weitere Details hier: Task Exception Handling in .NET 4.5

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X