Warum wartet Task.WhenAll nicht auf eine AggregateException?

100

In diesem Code:

private async void button1_Click(object sender, EventArgs e) {
    try {
        await Task.WhenAll(DoLongThingAsyncEx1(), DoLongThingAsyncEx2());
    }
    catch (Exception ex) {
        // Expect AggregateException, but got InvalidTimeZoneException
    }
}

Task DoLongThingAsyncEx1() {
    return Task.Run(() => { throw new InvalidTimeZoneException(); });
}

Task DoLongThingAsyncEx2() {
    return Task.Run(() => { throw new InvalidOperation();});
}

Ich hatte erwartet, eine WhenAllzu erstellen und zu werfen AggregateException, da mindestens eine der Aufgaben, auf die es wartete, eine Ausnahme auslöste. Stattdessen erhalte ich eine einzelne Ausnahme, die von einer der Aufgaben ausgelöst wurde.

Erstellt WhenAllnicht immer eine AggregateException?

Michael Ray Lovett
quelle
7
WhenAll nicht schaffen ein AggregateException. Wenn Sie Task.Waitanstelle von awaitin Ihrem Beispiel verwendet, würden Sie fangenAggregateException
Peter Ritchie
2
+1, das ist es, was ich herausfinden möchte. Sparen Sie mir Stunden beim Debuggen und googeln.
Kennyzx
Zum ersten Mal seit einigen Jahren brauchte ich alle Ausnahmen von Task.WhenAllund bin in dieselbe Falle geraten. Also habe ich versucht , auf dieses Verhalten einzugehen.
noseratio

Antworten:

75

Ich weiß nicht genau, wo, aber ich habe irgendwo gelesen, dass sie mit neuen asynchronen / wartenden Schlüsselwörtern die AggregateExceptionin die eigentliche Ausnahme auspacken .

Im catch-Block erhalten Sie also die eigentliche Ausnahme und nicht die aggregierte. Dies hilft uns, natürlicheren und intuitiveren Code zu schreiben.

Dies war auch erforderlich, um die Konvertierung von vorhandenem Code in die Verwendung von async / await zu vereinfachen, wenn viele Codes bestimmte Ausnahmen und keine aggregierten Ausnahmen erwarten .

- Bearbeiten -

Verstanden:

Eine asynchrone Grundierung von Bill Wagner

Bill Wagner sagte: (in Wenn Ausnahmen passieren )

... Wenn Sie await verwenden, entpackt der vom Compiler generierte Code die AggregateException und löst die zugrunde liegende Ausnahme aus. Durch die Nutzung von await vermeiden Sie den zusätzlichen Aufwand für die Verarbeitung des AggregateException-Typs, der von Task.Result, Task.Wait und anderen in der Task-Klasse definierten Wait-Methoden verwendet wird. Dies ist ein weiterer Grund, wait anstelle der zugrunde liegenden Task-Methoden zu verwenden.

Decyklon
quelle
3
Ja, ich weiß, dass einige Änderungen an der Ausnahmebehandlung vorgenommen wurden, aber die neuesten Dokumente für Task.WhenAll-Status "Wenn eine der bereitgestellten Aufgaben in einem fehlerhaften Status ausgeführt wird, wird die zurückgegebene Task auch in einem fehlerhaften Status abgeschlossen, in dem ihre Ausnahmen enthalten sind die Aggregation der Menge der nicht verpackten Ausnahmen von jeder der gelieferten Aufgaben ".... In meinem Fall werden beide meiner Aufgaben in einem fehlerhaften Zustand abgeschlossen ...
Michael Ray Lovett
4
@MichaelRayLovett: Sie speichern die zurückgegebene Aufgabe nirgendwo. Ich wette, wenn Sie sich die Exception-Eigenschaft dieser Aufgabe ansehen, erhalten Sie eine AggregateException. In Ihrem Code verwenden Sie jedoch "Warten". Dadurch wird die AggregateException in die eigentliche Ausnahme entpackt.
Decyclone
3
Ich habe auch daran gedacht, aber zwei Probleme sind aufgetreten: 1) Ich kann anscheinend nicht herausfinden, wie die Aufgabe gespeichert werden soll, damit ich sie untersuchen kann (dh "Task myTask = warte auf Task.WhenAll (...)" nicht Es scheint nicht zu funktionieren. und 2) Ich glaube, ich sehe nicht ein, wie das Warten jemals mehrere Ausnahmen als nur eine Ausnahme darstellen könnte. Welche Ausnahme sollte es melden? Eine zufällig auswählen?
Michael Ray Lovett
2
Ja, wenn ich die Aufgabe speichere und sie im try / catch der Wartezeit untersuche, sehe ich, dass die Ausnahme AggregatedException ist. Die Dokumente, die ich lese, sind also richtig. Task.WhenAll packt die Ausnahmen in eine AggregateException. Aber dann wartet das Warten darauf, sie auszupacken. Ich lese gerade Ihren Artikel, aber ich sehe noch nicht, wie das Warten eine einzige Ausnahme aus den AggregateExceptions auswählen und diese gegen eine andere werfen kann.
Michael Ray Lovett
2
Lesen Sie den Artikel, danke. Aber ich verstehe immer noch nicht, warum das Warten eine AggregateException (die mehrere Ausnahmen darstellt) als nur eine einzige Ausnahme darstellt. Wie ist das ein umfassender Umgang mit Ausnahmen? .. Ich denke, wenn ich genau wissen möchte, welche Aufgaben Ausnahmen ausgelöst haben und welche sie ausgelöst haben, müsste ich das von Task.WhenAll ??
Michael Ray Lovett
54

Ich weiß, dass dies eine Frage ist, die bereits beantwortet wurde, aber die gewählte Antwort löst das Problem des OP nicht wirklich. Deshalb dachte ich, ich würde dies posten.

Diese Lösung gibt Ihnen die aggregierte Ausnahme (dh alle Ausnahmen, die von den verschiedenen Aufgaben ausgelöst wurden) und blockiert nicht (der Workflow ist immer noch asynchron).

async Task Main()
{
    var task = Task.WhenAll(A(), B());

    try
    {
        var results = await task;
        Console.WriteLine(results);
    }
    catch (Exception)
    {
        if (task.Exception != null)
        {
            throw task.Exception;
        }
    }
}

public async Task<int> A()
{
    await Task.Delay(100);
    throw new Exception("A");
}

public async Task<int> B()
{
    await Task.Delay(100);
    throw new Exception("B");
}

Der Schlüssel besteht darin, einen Verweis auf die Aggregataufgabe zu speichern, bevor Sie darauf warten. Anschließend können Sie auf die Exception-Eigenschaft zugreifen, die Ihre AggregateException enthält (auch wenn nur eine Task eine Ausnahme ausgelöst hat).

Hoffe das ist noch nützlich. Ich weiß, dass ich heute dieses Problem hatte.

Richiban
quelle
Hervorragende klare Antwort, dies sollte IMO die ausgewählte sein.
Bytedev
3
+1, aber kannst du das nicht einfach throw task.Exception;in den catchBlock stecken? (Es verwirrt mich, einen leeren Fang zu sehen, wenn Ausnahmen tatsächlich behandelt werden.)
AnorZaken
@AnorZaken Absolut; Ich erinnere mich nicht, warum ich es ursprünglich so geschrieben habe, aber ich kann keinen Nachteil erkennen, also habe ich es in den Fangblock verschoben. Vielen Dank
Richiban
Ein kleiner Nachteil dieses Ansatzes ist, dass der Abbruchstatus ( Task.IsCanceled) nicht ordnungsgemäß weitergegeben wird. Dies kann löst die Verwendung eines Verlängerungs Helfer wie sein diese .
noseratio
34

Sie können alle Aufgaben durchlaufen, um festzustellen, ob mehrere eine Ausnahme ausgelöst haben:

private async Task Example()
{
    var tasks = new [] { DoLongThingAsyncEx1(), DoLongThingAsyncEx2() };

    try 
    {
        await Task.WhenAll(tasks);
    }
    catch (Exception ex) 
    {
        var exceptions = tasks.Where(t => t.Exception != null)
                              .Select(t => t.Exception);
    }
}

private Task DoLongThingAsyncEx1()
{
    return Task.Run(() => { throw new InvalidTimeZoneException(); });
}

private Task DoLongThingAsyncEx2()
{
    return Task.Run(() => { throw new InvalidOperationException(); });
}
jgauffin
quelle
2
Das funktioniert nicht. WhenAllwird bei der ersten Ausnahme beendet und gibt diese zurück. siehe: stackoverflow.com/questions/6123406/waitall-vs-whenall
jenson-button-event
14
Die beiden vorherigen Kommentare sind falsch. Der Code funktioniert tatsächlich und exceptionsenthält beide Ausnahmen.
Tobias
DoLongThingAsyncEx2 () muss eine neue InvalidOperationException () anstelle einer neuen InvalidOperation ()
auslösen
8
Um hier jeden Zweifel auszuräumen , habe ich eine erweiterte Geige zusammengestellt, die hoffentlich genau zeigt, wie sich dieses Handling entwickelt: dotnetfiddle.net/X2AOvM . Sie können sehen, awaitdass die erste Ausnahme dadurch entpackt wird, aber alle Ausnahmen sind tatsächlich noch über das Array von Aufgaben verfügbar.
Nuklearpidgeon
13

Ich dachte nur, ich würde die Antwort von @ Richiban erweitern, um zu sagen, dass Sie die AggregateException auch im catch-Block behandeln können, indem Sie sie aus der Aufgabe heraus referenzieren. Z.B:

async Task Main()
{
    var task = Task.WhenAll(A(), B());

    try
    {
        var results = await task;
        Console.WriteLine(results);
    }
    catch (Exception ex)
    {
        // This doesn't fire until both tasks
        // are complete. I.e. so after 10 seconds
        // as per the second delay

        // The ex in this instance is the first
        // exception thrown, i.e. "A".
        var firstExceptionThrown = ex;

        // This aggregate contains both "A" and "B".
        var aggregateException = task.Exception;
    }
}

public async Task<int> A()
{
    await Task.Delay(100);
    throw new Exception("A");
}

public async Task<int> B()
{
    // Extra delay to make it clear that the await
    // waits for all tasks to complete, including
    // waiting for this exception.
    await Task.Delay(10000);
    throw new Exception("B");
}
Daniel Šmon
quelle
11

Du denkst an Task.WaitAll- es wirft ein AggregateException.

WhenAll löst nur die erste Ausnahme der Liste der Ausnahmen aus, auf die es stößt.

Mohit Datta
quelle
3
Dies ist falsch. Die von der WhenAllMethode zurückgegebene Aufgabe verfügt über eine ExceptionEigenschaft, AggregateExceptiondie alle in ihr ausgelösten Ausnahmen enthält InnerExceptions. Was hier passiert ist, dass awaitdie erste innere Ausnahme anstelle der AggregateExceptionselbst ausgelöst wird (wie der Decyklon sagte). Wenn Sie die WaitMethode der Aufgabe aufrufen, anstatt darauf zu warten, wird die ursprüngliche Ausnahme ausgelöst.
Şafak Gür
2

Viele gute Antworten hier, aber ich möchte trotzdem meine Beschimpfungen veröffentlichen, da ich gerade auf dasselbe Problem gestoßen bin und einige Nachforschungen angestellt habe. Oder springen Sie zur folgenden TLDR-Version.

Das Problem

Das Warten auf die taskRückgabe durch löst Task.WhenAllnur die erste Ausnahme der AggregateExceptiongespeicherten task.ExceptionDaten aus, selbst wenn mehrere Aufgaben fehlerhaft sind.

Die aktuellen Dokumente zum BeispielTask.WhenAll :

Wenn eine der bereitgestellten Aufgaben in einem fehlerhaften Zustand ausgeführt wird, wird die zurückgegebene Aufgabe auch in einem fehlerhaften Zustand abgeschlossen, in dem ihre Ausnahmen die Aggregation der nicht ausgepackten Ausnahmen aus jeder der bereitgestellten Aufgaben enthalten.

Das ist richtig, sagt aber nichts über das oben erwähnte "Auspacken" -Verhalten aus, wenn die zurückgegebene Aufgabe erwartet wird.

Ich nehme an, die Dokumente erwähnen es nicht, weil dieses Verhalten nicht spezifisch istTask.WhenAll .

Es ist einfach Task.Exceptionvom Typ AggregateExceptionund für awaitFortsetzungen wird es immer als erste innere Ausnahme von Natur aus ausgepackt. Dies ist in den meisten Fällen großartig, da es normalerweise Task.Exceptionnur eine innere Ausnahme gibt. Beachten Sie jedoch diesen Code:

Task WhenAllWrong()
{
    var tcs = new TaskCompletionSource<DBNull>();
    tcs.TrySetException(new Exception[]
    {
        new InvalidOperationException(),
        new DivideByZeroException()
    });
    return tcs.Task;
}

var task = WhenAllWrong();    
try
{
    await task;
}
catch (Exception exception)
{
    // task.Exception is an AggregateException with 2 inner exception 
    Assert.IsTrue(task.Exception.InnerExceptions.Count == 2);
    Assert.IsInstanceOfType(task.Exception.InnerExceptions[0], typeof(InvalidOperationException));
    Assert.IsInstanceOfType(task.Exception.InnerExceptions[1], typeof(DivideByZeroException));

    // However, the exception that we caught here is 
    // the first exception from the above InnerExceptions list:
    Assert.IsInstanceOfType(exception, typeof(InvalidOperationException));
    Assert.AreSame(exception, task.Exception.InnerExceptions[0]);
}

Hier wird eine Instanz von genau so AggregateExceptionauf ihre erste innere Ausnahme entpackt, InvalidOperationExceptionwie wir es vielleicht hatten Task.WhenAll. Wir hätten es nicht beobachten können, DivideByZeroExceptionwenn wir nicht task.Exception.InnerExceptionsdirekt durchgegangen wären .

Stephen Toub von Microsoft erklärt den Grund für dieses Verhalten im zugehörigen GitHub-Problem :

Der Punkt, den ich ansprechen wollte, ist, dass er vor Jahren ausführlich besprochen wurde, als diese ursprünglich hinzugefügt wurden. Wir haben ursprünglich das getan, was Sie vorschlagen, wobei die von WhenAll zurückgegebene Aufgabe eine einzelne AggregateException enthielt, die alle Ausnahmen enthielt, dh task.Exception einen AggregateException-Wrapper zurückgab, der eine andere AggregateException enthielt, die dann die tatsächlichen Ausnahmen enthielt. Wenn es dann erwartet wurde, wurde die innere AggregateException weitergegeben. Das starke Feedback, das uns veranlasste, das Design zu ändern, war, dass a) die überwiegende Mehrheit dieser Fälle ziemlich homogene Ausnahmen aufwies, so dass die Vermehrung aller Aggregate nicht so wichtig war, b) die Vermehrung des Aggregats die Erwartungen an die Fänge brach für die spezifischen Ausnahmetypen und c) für Fälle, in denen jemand das Aggregat haben wollte, konnte er dies explizit mit den beiden Zeilen tun, wie ich es geschrieben habe. Wir hatten auch ausführliche Diskussionen darüber, wie sich das Warten auf Aufgaben mit mehreren Ausnahmen verhalten sollte, und hier sind wir gelandet.

Eine andere wichtige Sache zu beachten, ist dieses Auspackverhalten flach. Das heißt, es wird nur die erste Ausnahme auspacken AggregateException.InnerExceptionsund dort belassen , selbst wenn es sich zufällig um eine Instanz einer anderen handelt AggregateException. Dies kann noch eine weitere Verwirrungsebene hinzufügen. Ändern wir zum Beispiel Folgendes WhenAllWrong:

async Task WhenAllWrong()
{
    await Task.FromException(new AggregateException(
        new InvalidOperationException(),
        new DivideByZeroException()));
}

var task = WhenAllWrong();

try
{
    await task;
}
catch (Exception exception)
{
    // now, task.Exception is an AggregateException with 1 inner exception, 
    // which is itself an instance of AggregateException
    Assert.IsTrue(task.Exception.InnerExceptions.Count == 1);
    Assert.IsInstanceOfType(task.Exception.InnerExceptions[0], typeof(AggregateException));

    // And now the exception that we caught here is that inner AggregateException, 
    // which is also the same object we have thrown from WhenAllWrong:
    var aggregate = exception as AggregateException;
    Assert.IsNotNull(aggregate);
    Assert.AreSame(exception, task.Exception.InnerExceptions[0]);
    Assert.IsInstanceOfType(aggregate.InnerExceptions[0], typeof(InvalidOperationException));
    Assert.IsInstanceOfType(aggregate.InnerExceptions[1], typeof(DivideByZeroException));
}

Eine Lösung (TLDR)

Zurück zu await Task.WhenAll(...), was ich persönlich wollte, ist in der Lage zu sein:

  • Holen Sie sich eine einzelne Ausnahme, wenn nur eine ausgelöst wurde.
  • Erhalten Sie eine, AggregateExceptionwenn mehr als eine Ausnahme von einer oder mehreren Aufgaben gemeinsam ausgelöst wurde.
  • Vermeiden Sie es, das Taskeinzige zu speichern, um es zu überprüfen Task.Exception.
  • Verbreiten Sie den Stornierungsstatus ordnungsgemäß ( Task.IsCanceled), da so etwas das nicht tun würde : Task t = Task.WhenAll(...); try { await t; } catch { throw t.Exception; }.

Dafür habe ich folgende Erweiterung zusammengestellt:

public static class TaskExt 
{
    /// <summary>
    /// A workaround for getting all of AggregateException.InnerExceptions with try/await/catch
    /// </summary>
    public static Task WithAggregatedExceptions(this Task @this)
    {
        // using AggregateException.Flatten as a bonus
        return @this.ContinueWith(
            continuationFunction: anteTask =>
                anteTask.IsFaulted &&
                anteTask.Exception is AggregateException ex &&
                (ex.InnerExceptions.Count > 1 || ex.InnerException is AggregateException) ?
                Task.FromException(ex.Flatten()) : anteTask,
            cancellationToken: CancellationToken.None,
            TaskContinuationOptions.ExecuteSynchronously,
            scheduler: TaskScheduler.Default).Unwrap();
    }    
}

Nun funktioniert Folgendes so, wie ich es möchte:

try
{
    await Task.WhenAll(
        Task.FromException(new InvalidOperationException()),
        Task.FromException(new DivideByZeroException()))
        .WithAggregatedExceptions();
}
catch (OperationCanceledException) 
{
    Trace.WriteLine("Canceled");
}
catch (AggregateException exception)
{
    Trace.WriteLine("2 or more exceptions");
    // Now the exception that we caught here is an AggregateException, 
    // with two inner exceptions:
    var aggregate = exception as AggregateException;
    Assert.IsNotNull(aggregate);
    Assert.IsInstanceOfType(aggregate.InnerExceptions[0], typeof(InvalidOperationException));
    Assert.IsInstanceOfType(aggregate.InnerExceptions[1], typeof(DivideByZeroException));
}
catch (Exception exception)
{
    Trace.WriteLine($"Just a single exception: ${exception.Message}");
}
noseratio
quelle
1
Fantastische Antwort
rollt
-3

Das funktioniert bei mir

private async Task WhenAllWithExceptions(params Task[] tasks)
{
    var result = await Task.WhenAll(tasks);
    if (result.IsFaulted)
    {
                throw result.Exception;
    }
}
Alexey Kulikov
quelle
1
WhenAllist nicht dasselbe wie WhenAny. await Task.WhenAny(tasks)wird abgeschlossen, sobald eine Aufgabe abgeschlossen ist. Wenn Sie also eine Aufgabe haben, die sofort abgeschlossen wird und erfolgreich ist, und eine andere einige Sekunden dauert, bevor eine Ausnahme ausgelöst wird, wird diese sofort ohne Fehler zurückgegeben.
StriplingWarrior
Dann wird Wurflinie nie hier getroffen werden - WhenAll die Ausnahme ausgelöst hätte
Thab