Pourquoi n'attend pas sur Task.WhenAll lance une AggregateException?

102

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à créer et à lancer un AggregateException, car au moins l'une des tâches qu'il attendait a lancé une exception. Au lieu de cela, je récupère une seule exception lancée par l'une des tâches.

Ne WhenAllcrée pas toujours un AggregateException?

Michael Ray Lovett
la source
7
WhenAll ne créer un AggregateException. Si vous avez utilisé Task.Waitau lieu de awaitdans votre exemple, vous attraperiezAggregateException
Peter Ritchie
2
+1, c'est ce que j'essaie de comprendre, me faire gagner des heures de débogage et de recherche sur Google.
kennyzx
Pour la première fois depuis quelques années, j'avais besoin de toutes les exceptions Task.WhenAllet je suis tombé dans le même piège. J'ai donc essayé d' entrer dans les détails sur ce comportement.
noseratio le

Réponses:

76

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

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

Cela était également nécessaire pour faciliter la conversion du code existant en utilisant async / await où le grand nombre de code attend des exceptions spécifiques et non des exceptions agrégées.

-- Éditer --

Je l'ai:

An Async Primer par Bill Wagner

Bill Wagner a déclaré: (dans When Exceptions Happen )

... Lorsque vous utilisez await, le code généré par le compilateur désencapsule AggregateException et lève l'exception sous-jacente. En tirant parti de await, vous évitez le travail supplémentaire pour gérer le type AggregateException utilisé par Task.Result, Task.Wait et d'autres méthodes Wait définies dans la classe Task. C'est une autre raison d'utiliser await au lieu des méthodes Task sous-jacentes ...

décyclone
la source
3
Oui, je sais qu'il y a eu quelques changements dans la gestion des exceptions, mais les documents les plus récents pour Task.WhenAll indiquent "Si l'une des tâches fournies se termine dans un état défectueux, la tâche retournée se terminera également dans un état Faulted, où ses exceptions contiendront l'agrégation de l'ensemble des exceptions non emballées de chacune des tâches fournies ".... Dans mon cas, mes deux tâches se terminent dans un état défectueux ...
Michael Ray Lovett
4
@MichaelRayLovett: Vous ne stockez nulle part la tâche retournée. Je parie que lorsque vous regardez la propriété Exception de cette tâche, vous obtiendrez une AggregateException. Mais, dans votre code, vous utilisez await. Cela permet à AggregateException d'être déroulé dans l'exception réelle.
décyclone
3
J'y ai pensé aussi, mais deux problèmes se sont posés: 1) Je n'arrive pas à comprendre comment stocker la tâche afin de pouvoir l'examiner (c'est-à-dire "Task myTask = await Task.WhenAll (...)" ne ne semble pas fonctionner. et 2) Je suppose que je ne vois pas comment await pourrait jamais représenter plusieurs exceptions comme une seule exception .. quelle exception devrait-il signaler? En choisir un au hasard?
Michael Ray Lovett
2
Oui, lorsque je stocke la tâche et que je l'examine dans le try / catch de l'attente, je vois que son exception est AggregatedException. Les documents que j'ai lus sont donc justes; Task.WhenAll encapsule les exceptions dans une AggregateException. Mais alors attendre les déballe. Je lis votre article maintenant, mais je ne vois pas encore comment await peut choisir une seule exception dans AggregateExceptions et lancer celle-là contre une autre ..
Michael Ray Lovett
3
Lisez l'article, merci. Mais je ne comprends toujours pas pourquoi await représente une AggregateException (représentant plusieurs exceptions) comme une seule exception. Comment est-ce un traitement complet des exceptions? .. Je suppose que si je veux savoir exactement quelles tâches ont généré des exceptions et celles qu'elles ont lancées, je devrais examiner l'objet Task créé par Task.WhenAll ??
Michael Ray Lovett
55

Je sais que c'est une question à laquelle on a déjà répondu, mais la réponse choisie ne résout pas vraiment le problème du PO, alors j'ai pensé que je publierais ceci.

Cette solution vous donne l'exception globale (c'est-à-dire toutes les exceptions qui ont été levées par les différentes tâches) et ne 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 levé une exception).

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

Richiban
la source
Excellente réponse claire, c'est l'OMI qui devrait être celle choisie.
bytedev
3
+1, mais ne pouvez-vous pas simplement mettre l' throw task.Exception;intérieur du catchbloc? (Cela me déroute de voir une capture vide lorsque les exceptions sont réellement gérées.)
AnorZaken
@AnorZaken Absolument; Je ne me souviens pas pourquoi je l'ai écrit comme ça à l'origine, mais je ne vois aucun inconvénient, donc je l'ai déplacé dans le bloc catch. Merci
Richiban
Un inconvénient mineur de cette approche est que l'état d'annulation ( Task.IsCanceled) n'est pas correctement propagé. Cela peut être résolu en utilisant un assistant d'extension comme celui-ci .
noseratio
34

Vous pouvez parcourir toutes les tâches pour voir si plusieurs d'entre elles ont généré 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(); });
}
Jgauffin
la source
2
cela ne fonctionne pas. WhenAllquitte à la première exception et retourne cela. voir: stackoverflow.com/questions/6123406/waitall-vs-whenall
jenson-button-event
14
Les deux commentaires précédents sont incorrects. Le code fonctionne en fait et exceptionscontient les deux exceptions levées.
Tobias
DoLongThingAsyncEx2 () doit lancer une nouvelle InvalidOperationException () au lieu de la nouvelle InvalidOperation ()
Artemious
8
Pour dissiper tout doute ici, j'ai mis en place un violon étendu qui, espérons-le, montre exactement comment cette manipulation se déroule: dotnetfiddle.net/X2AOvM . Vous pouvez voir que le awaitprovoque le déballage de la première exception, mais toutes les exceptions sont en effet toujours disponibles via le tableau de tâches.
nucleapidgeon
13

Je pensais juste que je développerais la réponse de @ Richiban pour dire que vous pouvez également gérer l'exception AggregateException dans le bloc catch en le référençant à partir de 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");
}
Daniel Šmon
la source
11

Vous pensez à Task.WaitAll- cela lance un AggregateException.

WhenAll lève simplement la première exception de la liste des exceptions qu'il rencontre.

Mohit Datta
la source
3
C'est faux, la tâche retournée par la WhenAllméthode a une Exceptionpropriété qui AggregateExceptioncontient toutes les exceptions levées dans son InnerExceptions. Ce qui se passe ici, c'est que awaitjeter la première exception intérieure au lieu de AggregateExceptionelle - même (comme dit le décyclone). L'appel de la Waitméthode de la tâche au lieu de l'attendre entraîne la levée de l'exception d'origine.
Şafak Gür
3

Beaucoup de bonnes réponses ici, mais j'aimerais tout de même publier ma diatribe car je viens de rencontrer le même problème et j'ai mené des recherches. Ou passez à la version TLDR ci-dessous.

Le problème

Attendre le taskretour par Task.WhenAlllève uniquement la première exception du AggregateExceptionstocké dans task.Exception, même lorsque plusieurs tâches ont échoué.

La documentation actuelle pourTask.WhenAll dire:

Si l'une des tâches fournies se termine dans un état défectueux, la tâche retournée se terminera également dans un état défectueux, où ses exceptions contiendront l'agrégation de l'ensemble d'exceptions déballées de chacune des tâches fournies.

Ce qui est correct, mais cela ne dit rien sur le comportement de "déroulement" mentionné ci-dessus lorsque la tâche retournée est attendue.

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

C'est simplement ce qui Task.Exceptionest de type AggregateExceptionet pour les awaitsuites, il est toujours déballé comme sa première exception intérieure, par conception. C'est génial dans la plupart des cas, car il se Task.Exceptioncompose généralement d'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 AggregateExceptionest déroulée dans sa première exception interne InvalidOperationExceptionexactement de la même manière que nous l'aurions pu avoir avec Task.WhenAll. Nous aurions pu ne pas observer DivideByZeroExceptionsi nous ne passions pas task.Exception.InnerExceptionsdirectement.

Stephen Toub de Microsoft explique la raison de ce comportement dans le problème GitHub associé :

Ce que j'essayais de faire valoir, c'est que cela a été discuté en profondeur, il y a des années, lorsque ceux-ci ont été ajoutés à l'origine. Nous avons initialement fait ce que vous suggérez, avec la tâche retournée par WhenAll contenant une seule AggregateException qui contenait toutes les exceptions, c'est-à-dire que task.Exception renverrait un wrapper AggregateException qui contenait une autre AggregateException qui contenait alors les exceptions réelles; puis quand elle était attendue, l'exception AggregateException interne serait propagée. La forte rétroaction que nous avons reçue et qui nous a amenés à changer la conception était que a) la grande majorité de ces cas avaient des exceptions assez homogènes, de sorte que la propagation du tout dans un agrégat n'était pas si important, b) la propagation de l'agrégat, puis brisé les attentes concernant les captures. pour les types d'exceptions spécifiques, et c) pour les cas où quelqu'un voulait l'agrégat, il pouvait le faire explicitement avec les deux lignes comme je l'ai écrit. Nous avons également eu des discussions approfondies sur ce que devrait être le comportement de await en ce qui concerne les tâches contenant plusieurs exceptions, 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éroulera que la première exception AggregateException.InnerExceptionset la laissera là, même s'il s'agit d'une instance d'une autre AggregateException. Cela peut ajouter encore une autre couche de confusion. Par exemple, changeons WhenAllWrongcomme ceci:

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, revenons à await Task.WhenAll(...), ce que je voulais personnellement, c'est pouvoir:

  • Obtenez une seule exception si une seule a été lancée;
  • Obtenez un AggregateExceptionsi plus d'une exception a été levée collectivement par une ou plusieurs tâches;
  • Évitez d'avoir à enregistrer le Taskseul pour vérifier son Task.Exception;
  • Propager l'état d'annulation correctement ( Task.IsCanceled), comme quelque chose comme cela ne ferait pas cela: Task t = Task.WhenAll(...); try { await t; } catch { throw t.Exception; }.

J'ai mis en place 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 souhaite:

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
la source
2
Réponse fantastique
lance le
-3

Cela fonctionne pour moi

private async Task WhenAllWithExceptions(params Task[] tasks)
{
    var result = await Task.WhenAll(tasks);
    if (result.IsFaulted)
    {
                throw result.Exception;
    }
}
Alexey Kulikov
la source
1
WhenAlln'est pas la même chose que WhenAny. await Task.WhenAny(tasks)s'achèvera dès qu'une tâche sera terminée. Donc, si vous avez une tâche qui se termine immédiatement et qui réussit et qu'une autre prend quelques secondes avant de lancer une exception, elle reviendra immédiatement sans aucune erreur.
StriplingWarrior
Alors la ligne de lancer ne sera jamais touchée ici - WhenAll aurait jeté l'exception
thab
-5

Dans votre code, la première exception est renvoyée par conception, comme expliqué à l' adresse http://blogs.msdn.com/b/pfxteam/archive/2011/09/28/task-exception-handling-in-net-4-5. aspx

Quant à votre question, vous obtiendrez l'exception AggreateException si vous écrivez du code comme celui-ci:

try {
    var result = Task.WhenAll(DoLongThingAsyncEx1(), DoLongThingAsyncEx2()).Result; 
}
catch (Exception ex) {
    // Expect AggregateException here
} 
Nébuleuse
la source