¿Por qué no espera en Task.WhenAll arroja una AggregateException?

101

En este código:

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

Esperaba WhenAllcrear y lanzar un AggregateException, ya que al menos una de las tareas que estaba esperando arrojó una excepción. En cambio, obtengo una única excepción lanzada por una de las tareas.

¿ WhenAllNo siempre crea una AggregateException?

Michael Ray Lovett
fuente
7
WhenAll no crear una AggregateException. Si usara en Task.Waitlugar de awaiten su ejemplo, lo atraparíaAggregateException
Peter Ritchie
2
+1, esto es lo que estoy tratando de averiguar, me ahorrará horas de depuración y búsqueda de Google.
kennyzx
Por primera vez en bastantes años necesitaba todas las excepciones Task.WhenAlly caí en la misma trampa. Así que intenté entrar en detalles profundos sobre este comportamiento.
ratio nasal

Respuestas:

75

No recuerdo exactamente dónde, pero leí en alguna parte que con las nuevas palabras clave async / await , desenvuelven la AggregateExceptionexcepción real.

Entonces, en el bloque de captura, obtiene la excepción real y no la agregada. Esto nos ayuda a escribir código más natural e intuitivo.

Esto también era necesario para facilitar la conversión del código existente en el uso de async / await donde una gran cantidad de código espera excepciones específicas y no excepciones agregadas.

- Editar -

Entendido:

Una cartilla asincrónica de Bill Wagner

Bill Wagner dijo: (en Cuando ocurren excepciones )

... Cuando usa await, el código generado por el compilador desenvuelve la AggregateException y lanza la excepción subyacente. Al aprovechar await, evita el trabajo adicional para manejar el tipo AggregateException utilizado por Task.Result, Task.Wait y otros métodos Wait definidos en la clase Task. Esa es otra razón para usar await en lugar de los métodos Task subyacentes ...

deciclón
fuente
3
Sí, sé que ha habido algunos cambios en el manejo de excepciones, pero los documentos más recientes para Task.WhenAll indican "Si alguna de las tareas proporcionadas se completa en un estado de falla, la tarea devuelta también se completará en un estado de falla, donde sus excepciones contendrán la agregación del conjunto de excepciones no envueltas de cada una de las tareas proporcionadas ".... En mi caso, mis dos tareas se están completando en un estado defectuoso ...
Michael Ray Lovett
4
@MichaelRayLovett: No está almacenando la tarea devuelta en ningún lugar. Apuesto a que cuando miras la propiedad Exception de esa tarea, obtendrás una AggregateException. Pero, en su código, está usando await. Eso hace que AggregateException se desenvuelva en la excepción real.
deciclón
3
También pensé en eso, pero surgieron dos problemas: 1) Parece que no puedo averiguar cómo almacenar la tarea para poder examinarla (es decir, "Task myTask = await Task.WhenAll (...)" doesn parece que funciona. y 2) Supongo que no veo cómo await podría representar múltiples excepciones como una sola excepción ... ¿Qué excepción debería informar? ¿Elegir uno al azar?
Michael Ray Lovett
2
Sí, cuando almaceno la tarea y la examino en el intento / captura de la espera, veo que su excepción es AggregatedException. Entonces los documentos que leí son correctos; Task.WhenAll está resumiendo las excepciones en una AggregateException. Pero luego esperar es desenvolverlos. Estoy leyendo su artículo ahora, pero todavía no veo cómo await puede elegir una sola excepción de AggregateExceptions y lanzar esa contra otra ..
Michael Ray Lovett
3
Lea el artículo, gracias. Pero todavía no entiendo por qué await representa una AggregateException (que representa múltiples excepciones) como una sola excepción. ¿Cómo es eso un manejo integral de las excepciones? .. Supongo que si quiero saber exactamente qué tareas arrojaron excepciones y cuáles arrojaron, tendría que examinar el objeto Task creado por Task.WhenAll ??
Michael Ray Lovett
55

Sé que esta es una pregunta que ya está respondida, pero la respuesta elegida realmente no resuelve el problema del OP, así que pensé en publicar esto.

Esta solución le brinda la excepción agregada (es decir, todas las excepciones generadas por las diversas tareas) y no bloquea (el flujo de trabajo sigue siendo asincrónico).

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 clave es guardar una referencia a la tarea agregada antes de esperarla, luego puede acceder a su propiedad Exception que contiene su AggregateException (incluso si solo una tarea arrojó una excepción).

Espero que esto siga siendo útil. Sé que hoy tuve este problema.

Richiban
fuente
Excelente respuesta clara, esta debería ser la OMI seleccionada.
bytedev
3
+1, pero ¿no puedes simplemente poner el throw task.Exception;interior del catchbloque? (Me confunde ver una captura vacía cuando las excepciones realmente se están manejando.)
AnorZaken
@AnorZaken Absolutamente; No recuerdo por qué lo escribí así originalmente, pero no veo ningún inconveniente, así que lo moví en el bloque de captura. Gracias
Richiban
Una desventaja menor de este enfoque es que el estado de cancelación ( Task.IsCanceled) no se propaga correctamente. Esto se puede resolver usando un ayudante de extensión como este .
noseratio
34

Puede recorrer todas las tareas para ver si más de una ha generado una excepción:

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
fuente
2
esto no funciona. WhenAllsale con la primera excepción y devuelve eso. ver: stackoverflow.com/questions/6123406/waitall-vs-whenall
jenson-button-event
14
Los dos comentarios anteriores son incorrectos. De hecho, el código funciona y exceptionscontiene ambas excepciones lanzadas.
Tobias
DoLongThingAsyncEx2 () debe lanzar una nueva InvalidOperationException () en lugar de una nueva InvalidOperation ()
Artemious
8
Para aliviar cualquier duda aquí, armé un violín extendido que, con suerte, muestra exactamente cómo se desarrolla este manejo: dotnetfiddle.net/X2AOvM . Puede ver que awaithace que se desenvuelva la primera excepción, pero todas las excepciones todavía están disponibles a través de la matriz de tareas.
nuclearpidgeon
13

Solo pensé en ampliar la respuesta de @ Richiban para decir que también puede manejar la excepción AggregateException en el bloque catch haciendo referencia a ella desde la tarea. P.ej:

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
fuente
11

Estás pensando en Task.WaitAll... arroja un AggregateException.

WhenAll simplemente lanza la primera excepción de la lista de excepciones que encuentra.

Mohit Datta
fuente
3
Esto es incorrecto, la tarea devuelta por el WhenAllmétodo tiene una Exceptionpropiedad que AggregateExceptioncontiene todas las excepciones lanzadas en su InnerExceptions. Lo que está sucediendo aquí es awaitlanzar la primera excepción interna en lugar de la AggregateExceptionpropia (como dijo Decyclone). Llamar al Waitmétodo de la tarea en lugar de esperarlo hace que se lance la excepción original.
Şafak Gür
3

Muchas buenas respuestas aquí, pero aún me gustaría publicar mi perorata, ya que me encontré con el mismo problema y realicé algunas investigaciones. O pase a la versión TLDR a continuación.

El problema

Esperar la taskdevolución Task.WhenAllsolo arroja la primera excepción de la AggregateExceptionalmacenada task.Exception, incluso cuando varias tareas han fallado.

Los documentos actuales paraTask.WhenAll decir:

Si alguna de las tareas proporcionadas se completa en un estado de falla, la tarea devuelta también se completará en un estado de falla, donde sus excepciones contendrán la agregación del conjunto de excepciones sin empaquetar de cada una de las tareas proporcionadas.

Lo cual es correcto, pero no dice nada sobre el comportamiento de "desenvolvimiento" antes mencionado de cuándo se espera la tarea devuelta.

Supongo que los documentos no lo mencionan porque ese comportamiento no es específico deTask.WhenAll .

Es simplemente que Task.Exceptiones de tipo AggregateExceptiony para las awaitcontinuaciones siempre se desenvuelve como su primera excepción interna, por diseño. Esto es excelente para la mayoría de los casos, porque generalmente Task.Exceptionconsta de una sola excepción interna. Pero considere este código:

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

Aquí, una instancia de AggregateExceptionse desenvuelve en su primera excepción interna InvalidOperationExceptionexactamente de la misma manera que podríamos haberla tenido Task.WhenAll. Podríamos haber dejado de observar DivideByZeroExceptionsi no hubiéramos pasado task.Exception.InnerExceptionsdirectamente.

Stephen Toub de Microsoft explica la razón detrás de este comportamiento en el problema relacionado de GitHub :

El punto que estaba tratando de hacer es que se discutió en profundidad, hace años, cuando se agregaron originalmente. Originalmente hicimos lo que está sugiriendo, con la Task devuelta de WhenAll que contiene una única AggregateException que contiene todas las excepciones, es decir, task.Exception devolvería un contenedor AggregateException que contenía otra AggregateException que luego contenía las excepciones reales; luego, cuando se esperaba, se propagaría la AggregateException interna. La fuerte retroalimentación que recibimos que nos llevó a cambiar el diseño fue que a) la gran mayoría de estos casos tenían excepciones bastante homogéneas, de modo que propagar todo en un agregado no era tan importante, b) propagar el agregado luego rompió las expectativas en torno a las capturas para los tipos de excepción específicos, yc) para los casos en los que alguien quería el agregado, podía hacerlo explícitamente con las dos líneas como escribí. También tuvimos extensas discusiones sobre cuál debería ser el comportamiento de await con respecto a las tareas que contienen múltiples excepciones, y aquí es donde aterrizamos.

Otra cosa importante a tener en cuenta, este comportamiento de desenvolver es superficial. Es decir, solo desenvolverá la primera excepción de AggregateException.InnerExceptionsy la dejará allí, incluso si resulta ser una instancia de otra AggregateException. Esto puede agregar otra capa de confusión. Por ejemplo, cambiemos WhenAllWrongasí:

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 solución (TLDR)

Entonces, volviendo a await Task.WhenAll(...), lo que personalmente quería es poder:

  • Obtenga una única excepción si solo se ha lanzado una;
  • Obtenga un AggregateExceptionsi se ha lanzado más de una excepción de forma colectiva por una o más tareas;
  • Evite tener que guardar el Taskúnico para comprobarlo Task.Exception;
  • Propagar el estado de cancelación correctamente ( Task.IsCanceled), ya que algo como esto no haría eso: Task t = Task.WhenAll(...); try { await t; } catch { throw t.Exception; }.

He reunido la siguiente extensión para eso:

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

Ahora, lo siguiente funciona de la manera que quiero:

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}");
}
ratio nasal
fuente
2
Respuesta fantástica
lanza el
-3

Esto funciona para mi

private async Task WhenAllWithExceptions(params Task[] tasks)
{
    var result = await Task.WhenAll(tasks);
    if (result.IsFaulted)
    {
                throw result.Exception;
    }
}
Alexey Kulikov
fuente
1
WhenAllno es lo mismo que WhenAny. await Task.WhenAny(tasks)se completará tan pronto como se complete cualquier tarea. Entonces, si tiene una tarea que se completa de inmediato y tiene éxito y otra toma unos segundos antes de lanzar una excepción, esta volverá inmediatamente sin ningún error.
StriplingWarrior
Entonces la línea de tiro nunca será golpeada aquí - Cuando todo hubiera lanzado la excepción
2019