387 Stimmen

In Erwartung mehrerer Aufgaben mit unterschiedlichen Ergebnissen

Ich habe 3 Aufgaben:

private async Task FeedCat() {}
private async Task SellHouse() {}
private async Task BuyCar() {}

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 etwas gemeinsam miteinander

Wie rufe und warte ich auf die Beendigung der 3 Aufgaben 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 haben). 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 separat abwarten, 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 abgeschlossen sind, aber sie werden alle ausgeführt. In den meisten Situationen ist es nicht sinnvoll, wenn Sie bereits wissen, dass das Ergebnis vorliegt. In Randfällen kann es jedoch anders sein.

56voto

Joel Mueller Punkte 27700

Wenn Sie C# 7 verwenden, können Sie eine praktische Hilfsmethode 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. Sie müssten natürlich mehrere Überladungen für verschiedene Anzahlen von zu wartenden Tasks erstellen.

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

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

32voto

Marc Gravell Punkte 970173

Angesichts von drei Aufgaben - FeedCat(), SellHouse() und BuyCar() gibt es zwei interessante Fälle: entweder sie alle werden synchron abgeschlossen (aus irgendeinem Grund, vielleicht Caching oder ein Fehler), oder sie werden es 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; typischerweise möchten wir das await:

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

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

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

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

Im Gegensatz zu einigen der obigen Kommentare hat es keinen Unterschied, ob man await anstelle von Task.WhenAll verwendet, wie die Aufgaben ausgeführt werden (gleichzeitig, nacheinander usw.). Auf höchster Ebene vorwegnimmt Task.WhenAll eine gute Compiler-Unterstützung für async/await und war nützlich als diese Dinge noch nicht existierten. Es ist auch nützlich, wenn man ein willkürliches Array von Aufgaben hat, anstatt 3 diskrete Aufgaben.

Aber: wir haben immer noch das Problem, dass async/await viel Compilerlärm 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 Ausweichpfad einbauen:

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,
       // zum Abschluss gebracht zu werden

    return Awaited(x, y, z);
}

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

Dieser Ansatz "synchroner Pfad mit asynchronem Ausweichpfad" wird immer häufiger, insbesondere in hochleistungsfähigen Codes, wo synchrone Abschlüsse relativ häufig sind. Beachten Sie, dass es überhaupt nicht hilft, wenn der Abschluss immer wirklich asynchron ist.

Zusätzliche Dinge, die hier gelten:

  1. bei aktuellem C# ist ein häufiges Muster, dass die async-Ausweichmethode häufig als lokale Funktion implementiert wird:

    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,
           // zum Abschluss gebracht zu werden
    
        return Awaited(x, y, z);
    }
  2. bevorzugen Sie ValueTask gegenüber Task, wenn es eine gute Chance gibt, dass Dinge jemals vollständig synchron enden mit vielen verschiedenen Rückgabewerten:

    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,
           // zum Abschluss gebracht zu werden
    
        return Awaited(x, y, z);
    }
  3. wenn möglich, bevorzugen Sie IsCompletedSuccessfully gegenüber Status == TaskStatus.RanToCompletion; das existiert jetzt für Task in .NET Core und überall für ValueTask

14voto

samfromlv Punkte 913

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

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 fehlschlug. In diesem Fall ist alles in Ordnung.

  2. SellHouse ist nicht abgeschlossen und schlägt zu einem bestimmten Zeitpunkt mit einer Ausnahme fehl. Die Ausnahme wird nicht beobachtet und auf dem Finalisiererthread erneut ausgelöst.

  3. SellHouse ist nicht abgeschlossen und enthält Awaits darin. Falls Ihr Code in ASP.NET ausgeführt wird, wird SellHouse fehlschlagen, sobald einige der Awaits darin abgeschlossen sind. Dies geschieht, weil Sie praktisch einen Fire-and-Forget-Aufruf gemacht haben und der Synchronisierungskontext verloren ging, sobald FeedCat fehlgeschlagen ist.

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

System.AggregateException: Eine oder mehrere Ausnahmen einer Aufgabe wurden entweder nicht beobachtet, indem auf die Aufgabe gewartet oder auf ihre Exception-Eigenschaft zugegriffen wurde. Dadurch wurde die unbeobachtete Ausnahme vom Finalisiererthread 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 inneren 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 Ausnahmestapel.

Für .NET 4.0 und höher können Sie unbeobachtete Ausnahmen mit TaskScheduler.UnobservedTaskException abfangen. Ab .NET 4.5 werden unbeobachtete Ausnahmen standardmäßig geschluckt, für .NET 4.0 wird die unbeobachtete Ausnahme Ihren Prozess zum Absturz bringen.

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