181 votes

Pourquoi await sur Task.WhenAll ne lève-t-il pas une AggregateException ?

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

Je m'attendais WhenAll pour créer et lancer un AggregateException car au moins une des tâches qu'il attendait a déclenché une exception. Au lieu de cela, je reçois une seule exception lancée par l'une des tâches.

Fait WhenAll ne crée pas toujours un AggregateException ?

139voto

Richiban Points 463

Je sais que c'est une question à laquelle on a déjà répondu, mais la réponse choisie n'est pas vraiment résoudre le problème de l'OP, alors j'ai pensé poster ceci.

Cette solution vous donne l'exception agrégée (c'est-à-dire todo les exceptions qui ont été lancées par les différentes tâches) et ne se bloque pas (le flux de travail est toujours asynchrone).

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 clé est d'enregistrer une référence à la tâche d'agrégation avant de l'attendre. Vous pouvez alors accéder à sa propriété Exception qui contient votre AggregateException (même si une seule tâche a déclenché une exception).

J'espère que cela vous sera toujours utile. Je sais que j'ai eu ce problème aujourd'hui.

99voto

decyclone Points 18778

Je ne me souviens pas exactement où, mais j'ai lu quelque part qu'avec les nouvelles async/await mots-clés, ils déballent le AggregateException dans l'exception réelle.

Ainsi, dans le bloc catch, vous obtenez l'exception réelle et non l'exception agrégée. Cela nous aide à écrire un code plus naturel et plus intuitif.

Cela était également nécessaire pour faciliter la conversion de code existant en utilisant async/await où une grande partie du code s'attend à des exceptions spécifiques et non à des exceptions agrégées.

-- Editer --

Je l'ai :

Une introduction à l'asynchronisme par Bill Wagner

Bill Wagner a dit : (en Quand les exceptions arrivent )

...Lorsque vous utilisez await, le code généré par le compilateur déballe l'exception AggregateException et lance l'exception sous-jacente. AggregateException et lance l'exception sous-jacente. En utilisant await, vous évitez le travail supplémentaire nécessaire pour traiter le type AggregateException utilisé par Task.Result, Task.Wait et d'autres méthodes Wait définies dans la classe classe Task. C'est une autre raison d'utiliser await au lieu du méthodes sous-jacentes de Task....

60voto

Noseratio Points 23840

Beaucoup de bonnes réponses ici, mais je voudrais quand même poster mon coup de gueule car je viens de rencontrer le même problème et j'ai fait quelques recherches. Ou passez à la version TLDR ci-dessous.

Le problème

En attendant le task retourné par Task.WhenAll ne lance que la première exception de la AggregateException stocké dans task.Exception même lorsque plusieurs tâches sont en panne.

Le site documents actuels pour Task.WhenAll dites :

Si l'une des tâches fournies s'achève dans un état de défaillance, la tâche retournée se terminera également dans un état défaillant, où ses exceptions contiendra l'agrégation de l'ensemble des exceptions non enveloppées de chacune des tâches fournies.

Ce qui est correct, mais ne dit rien sur le comportement de "déballage" mentionné plus haut lorsque la tâche retournée est attendue.

Je suppose que les docs ne le mentionnent pas. car ce comportement n'est pas spécifique à Task.WhenAll .

C'est simplement que Task.Exception est de type AggregateException et pour await il est toujours déballé comme sa première exception interne, à dessein. C'est très bien dans la plupart des cas, car généralement Task.Exception consiste en une seule exception interne. Mais considérez ce 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]);
}

Ici, une instance de AggregateException se déroule jusqu'à sa première exception interne InvalidOperationException exactement de la même manière que nous aurions pu l'avoir avec Task.WhenAll . Nous aurions pu ne pas observer DivideByZeroException si on ne passait pas par là task.Exception.InnerExceptions directement.

Microsoft Stephen Toub explique la raison de ce comportement dans la question connexe sur GitHub :

Ce que je voulais dire, c'est que le sujet a été discuté en profondeur, il y a des années, quand ils ont été ajoutés à l'origine. A l'origine, nous avons fait ce que ce que vous suggérez, avec la tâche retournée par WhenAll contenant une une seule AggregateException qui contenait toutes les exceptions, à savoir task.Exception renvoie une enveloppe AggregateException qui contient une autre AggregateException qui contiendrait une autre AggregateException contenant les véritables exceptions. exceptions ; ensuite, lorsqu'elle est attendue, l'exception interne AggregateException interne se propageait. Le feedback important que nous avons reçu et qui nous a amené à modifier la conception était que a) la grande majorité de ces cas avaient des exceptions exceptions assez homogènes, de sorte que la propagation de toutes dans un dans un agrégat n'était pas si important, b) la propagation de l'agrégat rompait alors les attentes concernant les captures pour l'exception spécifique. brisait les attentes autour des captures pour les types d'exceptions spécifiques, et c) pour les cas où quelqu'un voulait l'agrégat, il pouvait le faire le faire explicitement avec les deux lignes que j'ai écrites. Nous avons également eu de longues discussions approfondies sur ce que le comportement de await devrait être par rapport aux tâches contenant des exceptions multiples, et c'est là que nous avons atterri.

Une autre chose importante à noter, ce comportement de déballage est peu profond. C'est-à-dire qu'il ne déballera que la première exception de l'élément AggregateException.InnerExceptions et le laisser là, même s'il s'agit d'une instance d'une autre AggregateException . Cela peut ajouter une nouvelle couche de confusion. Par exemple, changeons WhenAllWrong comme ça :

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

Une solution (TLDR)

Donc, retour à await Task.WhenAll(...) Ce que je voulais personnellement, c'était de pouvoir.. :

  • Obtenir une seule exception si une seule a été lancée ;
  • Obtenir un AggregateException si plus d'une exception a été lancée collectivement par une ou plusieurs tâches ;
  • Évitez de devoir sauvegarder le Task uniquement pour vérifier son Task.Exception ;
  • Propager correctement l'état d'annulation ( Task.IsCanceled ), car quelque chose comme ça ne le ferait pas : Task t = Task.WhenAll(...); try { await t; } catch { throw t.Exception; } .

J'ai créé l'extension suivante pour cela :

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

Maintenant, ce qui suit fonctionne comme je le veux :

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

51voto

jgauffin Points 51913

Vous pouvez parcourir toutes les tâches pour voir si plusieurs d'entre elles ont déclenché une exception :

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

16voto

Daniel Šmon Points 161

J'ai juste pensé que je pourrais développer la réponse de @Richiban pour dire que vous pouvez également gérer l'AggregateException dans le bloc catch en le référençant depuis la tâche. Par exemple

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X