Perché non attende Task.WhenAll genera un'eccezione AggregateException?


102

In questo codice:

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

Mi aspettavo WhenAlldi creare e lanciare un AggregateException, poiché almeno uno dei compiti in cui era in attesa ha generato un'eccezione. Invece, sto recuperando una singola eccezione generata da una delle attività.

Non WhenAllcrea sempre un AggregateException?


7
WhenAll non creare un AggregateException. Se usassi al Task.Waitposto del awaittuo esempio, prenderestiAggregateException
Peter Ritchie

2
+1, questo è quello che sto cercando di capire, risparmiandomi ore di debug e google.
kennyzx

Per la prima volta in parecchi anni avevo bisogno di tutte le eccezioni Task.WhenAlle sono caduto nella stessa trappola. Quindi ho provato ad approfondire i dettagli su questo comportamento.
noseratio

Risposte:


76

Non ricordo esattamente dove, ma ho letto da qualche parte che con le nuove parole chiave async / await , scartano l' AggregateExceptioneccezione effettiva.

Quindi, nel blocco catch, ottieni l'eccezione effettiva e non quella aggregata. Questo ci aiuta a scrivere codice più naturale e intuitivo.

Ciò era necessario anche per una conversione più semplice del codice esistente nell'uso di async / await dove la maggior parte del codice prevede eccezioni specifiche e non eccezioni aggregate.

-- Modificare --

Fatto:

Un primer asincrono di Bill Wagner

Bill Wagner ha detto: (in When Exceptions Happen )

... Quando si utilizza await, il codice generato dal compilatore scarica AggregateException e genera l'eccezione sottostante. Sfruttando await, si evita il lavoro aggiuntivo per gestire il tipo AggregateException utilizzato da Task.Result, Task.Wait e altri metodi Wait definiti nella classe Task. Questo è un altro motivo per usare await invece dei metodi Task sottostanti ...


3
Sì, so che ci sono state alcune modifiche alla gestione delle eccezioni, ma la documentazione più recente per Task.WhenAll dichiara "Se una delle attività fornite viene completata in uno stato di errore, l'attività restituita verrà completata anche in uno stato di errore, dove le sue eccezioni conterranno l'aggregazione della serie di eccezioni scartate da ciascuna delle attività fornite ".... Nel mio caso, entrambe le mie attività si stanno completando in uno stato di errore ...
Michael Ray Lovett

4
@MichaelRayLovett: non stai archiviando l'attività restituita da nessuna parte. Scommetto che quando guardi la proprietà Exception di quell'attività, otterrai un'eccezione AggregateException. Ma, nel tuo codice, stai usando await. Ciò fa sì che AggregateException venga scartato nell'eccezione effettiva.
deciclone

3
Ho pensato anche a questo, ma sono emersi due problemi: 1) Non riesco a capire come memorizzare l'attività in modo da poterla esaminare (es. "Task myTask = await Task.WhenAll (...)" non non sembra funzionare. e 2) Immagino di non vedere come await possa mai rappresentare più eccezioni come una sola eccezione .. quale eccezione dovrebbe segnalare? Sceglierne uno a caso?
Michael Ray Lovett

2
Sì, quando memorizzo l'attività e la esamino nel try / catch dell'attesa, vedo che l'eccezione è AggregatedException. Quindi i documenti che ho letto hanno ragione; Task.WhenAll racchiude le eccezioni in un AggregateException. Ma poi aspettare è scartarli. Sto leggendo il tuo articolo ora, ma non vedo ancora come await possa scegliere una singola eccezione dalle AggregateExceptions e lanciarla contro un'altra ..
Michael Ray Lovett

3
Leggi l'articolo, grazie. Ma ancora non capisco perché await rappresenti un'eccezione AggregateException (che rappresenta più eccezioni) come una singola eccezione. Come si tratta di una gestione completa delle eccezioni? .. Immagino che se voglio sapere esattamente quali attività hanno generato eccezioni e quali hanno generato, dovrei esaminare l'oggetto Task creato da Task.WhenAll ??
Michael Ray Lovett

55

So che questa è una domanda a cui è già stata data una risposta, ma la risposta scelta non risolve realmente il problema dell'OP, quindi ho pensato di postarla.

Questa soluzione ti dà l'eccezione aggregata (cioè tutte le eccezioni che sono state lanciate dalle varie attività) e non si blocca (il flusso di lavoro è ancora asincrono).

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

La chiave è salvare un riferimento all'attività aggregata prima di attenderla, quindi puoi accedere alla sua proprietà Eccezione che contiene la tua AggregateException (anche se solo un'attività ha generato un'eccezione).

Spero che questo sia ancora utile. So di aver avuto questo problema oggi.


Ottima risposta chiara, questo dovrebbe essere IMO quello selezionato.
bytedev

3
+1, ma non puoi semplicemente mettere l' throw task.Exception;interno del catchblocco? (Mi confonde vedere una cattura vuota quando le eccezioni vengono effettivamente gestite.)
AnorZaken

@AnorZaken Absolutely; Non ricordo perché l'ho scritto in quel modo originariamente, ma non riesco a vedere alcun aspetto negativo, quindi l'ho spostato nel blocco di cattura. Grazie
Richiban

Uno svantaggio minore di questo approccio è che lo stato di cancellazione ( Task.IsCanceled) non viene propagato correttamente. Questo può essere risolto usando un helper di estensione come questo .
noseratio

34

Puoi attraversare tutte le attività per vedere se più di una hanno generato un'eccezione:

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

2
questo non funziona. WhenAllesce alla prima eccezione e restituisce quella. vedi: stackoverflow.com/questions/6123406/waitall-vs-whenall
Jenson-button-evento

14
I due commenti precedenti non sono corretti. Il codice infatti funziona e exceptionscontiene entrambe le eccezioni lanciate.
Tobias

DoLongThingAsyncEx2 () deve generare una nuova InvalidOperationException () invece della nuova InvalidOperation ()
Artemious

8
Per alleviare qualsiasi dubbio qui, ho messo insieme un violino esteso che si spera mostri esattamente come funziona questa gestione: dotnetfiddle.net/X2AOvM . Puoi vedere che le awaitcause della prima eccezione da scartare, ma tutte le eccezioni sono effettivamente ancora disponibili tramite l'array di attività.
Nuclearpidgeon

13

Ho solo pensato di espandere la risposta di @ Richiban per dire che puoi anche gestire AggregateException nel blocco catch facendovi riferimento dall'attività. Per esempio:

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

11

Stai pensando Task.WaitAll- genera un AggregateException.

WhenAll genera solo la prima eccezione dell'elenco di eccezioni che incontra.


3
Questo è sbagliato, l'attività restituita dal WhenAllmetodo ha una Exceptionproprietà che è un AggregateExceptioncontenente tutte le eccezioni lanciate nel suo file InnerExceptions. Quello che sta succedendo qui è che awaitlanciare la prima eccezione interna invece della AggregateExceptionstessa (come ha detto il deciclone). La chiamata del Waitmetodo dell'attività invece di attendere provoca la generazione dell'eccezione originale.
Şafak Gür

3

Molte buone risposte qui, ma vorrei comunque pubblicare il mio sproloquio poiché ho appena incontrato lo stesso problema e condotto alcune ricerche. Oppure passa alla versione TLDR di seguito.

Il problema

Attendere il taskrestituito da Task.WhenAllgenera solo la prima eccezione del AggregateExceptioncontenuto task.Exception, anche quando più attività hanno avuto un errore.

I documenti attuali perTask.WhenAll dire:

Se una delle attività fornite viene completata in uno stato di errore, l'attività restituita verrà completata anche in uno stato di errore, dove le sue eccezioni conterranno l'aggregazione del set di eccezioni non incluse in ciascuna delle attività fornite.

Il che è corretto, ma non dice nulla sul comportamento di "scartare" di cui sopra quando si attende l'attività restituita.

Suppongo che i documenti non lo menzionino perché quel comportamento non è specifico perTask.WhenAll .

È semplicemente che Task.Exceptionè di tipo AggregateExceptione per le awaitcontinuazioni viene sempre scartato come prima eccezione interna, per design. Questo è ottimo per la maggior parte dei casi, perché di solito Task.Exceptionconsiste in una sola eccezione interna. Ma considera questo codice:

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

Qui, un'istanza di AggregateExceptionviene scartata alla sua prima eccezione interna InvalidOperationExceptionesattamente nello stesso modo in cui avremmo potuto averla con Task.WhenAll. Avremmo potuto non osservare DivideByZeroExceptionse non fossimo passati task.Exception.InnerExceptionsdirettamente.

Stephen Toub di Microsoft spiega il motivo di questo comportamento nel relativo problema di GitHub :

Il punto che stavo cercando di sottolineare è che è stato discusso in profondità, anni fa, quando questi furono aggiunti originariamente. Inizialmente abbiamo fatto ciò che suggerisci, con l'attività restituita da WhenAll contenente una singola AggregateException che conteneva tutte le eccezioni, ovvero task.Exception restituiva un wrapper AggregateException che conteneva un'altra AggregateException che conteneva le eccezioni effettive; quindi, quando era atteso, veniva propagata l'eccezione AggregateException interna. Il forte feedback che abbiamo ricevuto che ci ha portato a modificare il design è stato che a) la stragrande maggioranza di questi casi presentava eccezioni abbastanza omogenee, in modo tale che propagare tutto in un aggregato non era così importante, b) propagare l'aggregato ha poi infranto le aspettative sulle catture per i tipi di eccezione specifici, e c) per i casi in cui qualcuno desiderava l'aggregato, poteva farlo esplicitamente con le due righe come ho scritto. Abbiamo anche avuto discussioni approfondite su quale potrebbe essere il comportamento di await so per quanto riguarda le attività contenenti più eccezioni, ed è qui che siamo atterrati.

Un'altra cosa importante da notare, questo comportamento di scartare è superficiale. Cioè, scarterà solo la prima eccezione AggregateException.InnerExceptionse la lascerà lì, anche se si tratta di un'istanza di un'altra AggregateException. Ciò potrebbe aggiungere un ulteriore livello di confusione. Ad esempio, cambiamo in WhenAllWrongquesto modo:

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

Una soluzione (TLDR)

Quindi, tornando a await Task.WhenAll(...)ciò che personalmente volevo essere in grado di:

  • Ottieni una singola eccezione se ne è stata lanciata una sola;
  • Ottieni un messaggio AggregateExceptionse più di un'eccezione è stata lanciata collettivamente da una o più attività;
  • Evita di dover salvare il Tasksolo per controllarlo Task.Exception;
  • Propagare la cancellazione dello stato correttamente ( Task.IsCanceled), come qualcosa di simile non l'avrebbe fatto: Task t = Task.WhenAll(...); try { await t; } catch { throw t.Exception; }.

Ho messo insieme la seguente estensione per questo:

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

Ora, quanto segue funziona nel modo desiderato:

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

2
Risposta fantastica
rotola il

-3

Questo funziona per me

private async Task WhenAllWithExceptions(params Task[] tasks)
{
    var result = await Task.WhenAll(tasks);
    if (result.IsFaulted)
    {
                throw result.Exception;
    }
}

1
WhenAllnon è lo stesso di WhenAny. await Task.WhenAny(tasks)verrà completato non appena verrà completata qualsiasi attività. Quindi, se un'attività che viene completata immediatamente e ha esito positivo e un'altra impiega alcuni secondi prima di lanciare un'eccezione, questa tornerà immediatamente senza alcun errore.
StriplingWarrior

Quindi la linea di lancio non verrà mai colpita qui - WhenAll avrebbe lanciato l'eccezione
thab

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.