Por que não espera em Task.WhenAll lança uma AggregateException?

102

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

Eu esperava WhenAllcriar e lançar um AggregateException, já que pelo menos uma das tarefas que ele estava esperando gerou uma exceção. Em vez disso, estou recebendo de volta uma única exceção lançada por uma das tarefas.

Será que WhenAllnem sempre criar um AggregateException?

Michael Ray Lovett
fonte
7
WhenAll não criar um AggregateException. Se você usou em Task.Waitvez de awaitem seu exemplo, você pegariaAggregateException
Peter Ritchie
2
+1, isso é o que estou tentando descobrir, economize horas de depuração e google-ing.
kennyzx
Pela primeira vez em alguns anos, precisei de todas as exceções Task.WhenAlle caí na mesma armadilha. Tentei entrar em detalhes sobre esse comportamento.
noseratio

Respostas:

76

Não me lembro exatamente onde, mas li em algum lugar que, com novas palavras-chave async / await , elas se desdobram AggregateExceptionna exceção real.

Portanto, no bloco catch, você obtém a exceção real e não a agregada. Isso nos ajuda a escrever um código mais natural e intuitivo.

Isso também era necessário para facilitar a conversão do código existente em async / await, onde muitos códigos esperam exceções específicas e não exceções agregadas.

- Editar -

Entendi:

An Async Primer de Bill Wagner

Bill Wagner disse: (em When Exceptions Happen )

... Quando você usa await, o código gerado pelo compilador desembrulha o AggregateException e lança a exceção subjacente. Aproveitando o await, você evita o trabalho extra para lidar com o tipo AggregateException usado por Task.Result, Task.Wait e outros métodos Wait definidos na classe Task. Esse é outro motivo para usar o await em vez dos métodos Task subjacentes ....

deciclone
fonte
3
Sim, eu sei que houve algumas mudanças no tratamento de exceções, mas os documentos mais recentes para Task.WhenAll state "Se qualquer uma das tarefas fornecidas for concluída em um estado de falha, a tarefa retornada também será concluída em um estado de Falha, onde suas exceções conterão a agregação do conjunto de exceções não agrupadas de cada uma das tarefas fornecidas ".... No meu caso, ambas as minhas tarefas estão sendo concluídas em um estado de falha ...
Michael Ray Lovett
4
@MichaelRayLovett: Você não está armazenando a Tarefa retornada em nenhum lugar. Aposto que, ao olhar para a propriedade Exception dessa tarefa, você obteria uma AggregateException. Mas, em seu código, você está usando o await. Isso faz com que a AggregateException seja desdobrada na exceção real.
deciclone de
3
Também pensei nisso, mas surgiram dois problemas: 1) Não consigo descobrir como armazenar a tarefa para que possa examiná-la (ou seja, "Tarefa minhaTarefa = aguarda Tarefa.QuandoTodos (...)" não parece que não funciona. e 2) Acho que não vejo como o await poderia representar várias exceções como apenas uma exceção ... qual exceção ele deve relatar? Escolha um ao acaso?
Michael Ray Lovett
2
Sim, quando eu armazeno a tarefa e a examino no try / catch do await, vejo que a exceção é AggregatedException. Portanto, os documentos que li estão certos; Task.WhenAll está encerrando as exceções em uma AggregateException. Mas esperar é desembrulhá-los. Estou lendo seu artigo agora, mas não vejo ainda como o await pode escolher uma única exceção do AggregateExceptions e jogá-la contra outra ..
Michael Ray Lovett
3
Leia o artigo, obrigado. Mas ainda não entendo por que await representa uma AggregateException (representando várias exceções) como apenas uma única exceção. Como isso é um tratamento abrangente de exceções? .. Eu acho que se eu quiser saber exatamente quais tarefas lançaram exceções e quais elas lançaram, eu teria que examinar o objeto Task criado por Task.WhenAll ??
Michael Ray Lovett
55

Sei que essa é uma pergunta que já foi respondida, mas a resposta escolhida não resolve realmente o problema do OP, então pensei em postar isso.

Esta solução fornece a exceção agregada (ou seja, todas as exceções que foram lançadas pelas várias tarefas) e não bloqueia (o fluxo de trabalho ainda é assíncrono).

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

A chave é salvar uma referência à tarefa agregada antes de esperá-la, então você pode acessar sua propriedade Exception que contém sua AggregateException (mesmo se apenas uma tarefa gerou uma exceção).

Espero que ainda seja útil. Eu sei que tive esse problema hoje.

Richiban
fonte
Excelente resposta clara, esta deve ser IMO a escolhida.
bytedev
3
+1, mas você não pode simplesmente colocar throw task.Exception;o catchbloco dentro ? (
Fico confuso
@AnorZaken Absolutely; Não me lembro por que escrevi assim originalmente, mas não consigo ver nenhuma desvantagem, então mudei para o bloco catch. Obrigado
Richiban
Uma pequena desvantagem dessa abordagem é que o status de cancelamento ( Task.IsCanceled) não é propagado corretamente. Isso pode ser resolvido usando um auxiliar de extensão como este .
noseratio
34

Você pode percorrer todas as tarefas para ver se mais de uma gerou uma exceção:

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
fonte
2
isso não funciona. WhenAllsai na primeira exceção e retorna isso. consulte: stackoverflow.com/questions/6123406/waitall-vs-whenall
jenson-button-event
14
Os dois comentários anteriores estão incorretos. O código realmente funciona e exceptionscontém ambas as exceções lançadas.
Tobias
DoLongThingAsyncEx2 () deve lançar novo InvalidOperationException () em vez de novo InvalidOperation ()
Artemious
8
Para aliviar qualquer dúvida aqui, eu montei um violino estendido que, espero, mostra exatamente como esse tratamento funciona: dotnetfiddle.net/X2AOvM . Você pode ver que o awaitfaz com que a primeira exceção seja desembrulhada, mas todas as exceções ainda estão disponíveis por meio do array de Tarefas.
nuclearpidgeon
13

Apenas pensei em expandir a resposta de @ Richiban para dizer que você também pode manipular AggregateException no bloco catch referenciando-o na tarefa. Por exemplo:

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

Você está pensando Task.WaitAll- lança um AggregateException.

WhenAll apenas lança a primeira exceção da lista de exceções que encontra.

Mohit Datta
fonte
3
Isso está errado, a tarefa retornada do WhenAllmétodo tem uma Exceptionpropriedade que AggregateExceptioncontém todas as exceções lançadas em seu InnerExceptions. O que está acontecendo aqui é awaitlançar a primeira exceção interna em vez da AggregateExceptionprópria (como o deciclone disse). Chamar o Waitmétodo da tarefa em vez de esperá-lo faz com que a exceção original seja lançada.
Şafak Gür
3

Muitas respostas boas aqui, mas eu ainda gostaria de postar meu discurso retórico, pois acabei de encontrar o mesmo problema e conduzi algumas pesquisas. Ou pule para a versão TLDR abaixo.

O problema

Aguardar o taskretornado por Task.WhenAllapenas lança a primeira exceção do AggregateExceptionarmazenado em task.Exception, mesmo quando várias tarefas falharam.

Os documentos atuais paraTask.WhenAll dizer:

Se qualquer uma das tarefas fornecidas for concluída em um estado de falha, a tarefa retornada também será concluída em um estado de Falha, onde suas exceções conterão a agregação do conjunto de exceções não agrupadas de cada uma das tarefas fornecidas.

O que está correto, mas não diz nada sobre o comportamento de "desembrulhar" mencionado acima de quando a tarefa retornada é aguardada.

Suponho que os documentos não mencionem isso porque esse comportamento não é específico paraTask.WhenAll .

É simplesmente que Task.Exceptioné do tipo AggregateExceptione para awaitcontinuações sempre é desembrulhado como sua primeira exceção interna, por design. Isso é ótimo para a maioria dos casos, porque geralmente Task.Exceptionconsiste em apenas uma exceção interna. Mas 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]);
}

Aqui, uma instância de AggregateExceptioné desempacotada em sua primeira exceção interna InvalidOperationExceptionexatamente da mesma maneira que poderíamos ter feito Task.WhenAll. Poderíamos ter falhado em observar DivideByZeroExceptionse não tivéssemos passado task.Exception.InnerExceptionsdiretamente.

Stephen Toub, da Microsoft, explica a razão por trás desse comportamento no problema relacionado do GitHub :

O que eu estava tentando enfatizar é que isso foi discutido em profundidade, anos atrás, quando estes foram originalmente adicionados. Originalmente, fizemos o que você está sugerindo, com a Tarefa retornada de WhenAll contendo uma única AggregateException que continha todas as exceções, ou seja, task.Exception retornaria um wrapper AggregateException que continha outra AggregateException que continha as exceções reais; então, quando fosse aguardado, o AggregateException interno seria propagado. O forte feedback que recebemos que nos levou a mudar o design foi que a) a grande maioria desses casos tinha exceções razoavelmente homogêneas, de modo que a propagação de tudo em um agregado não era tão importante, b) a propagação do agregado quebrou as expectativas em torno das capturas para os tipos de exceção específicos, ec) para os casos em que alguém deseja o agregado, pode fazê-lo explicitamente com as duas linhas, como escrevi. Também tivemos extensas discussões sobre qual deveria ser o comportamento de await em relação a tarefas que continham múltiplas exceções, e foi aí que pousamos.

Outra coisa importante a se notar, esse comportamento de desembrulhar é superficial. Ou seja, ele apenas desembrulhará a primeira exceção AggregateException.InnerExceptionse a deixará lá, mesmo se acontecer de ser uma instância de outra AggregateException. Isso pode adicionar mais uma camada de confusão. Por exemplo, vamos mudar WhenAllWrongassim:

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

Uma solução (TLDR)

Então, voltando ao await Task.WhenAll(...), o que eu pessoalmente queria é ser capaz de:

  • Obtenha uma única exceção se apenas uma tiver sido lançada;
  • Obtenha um AggregateExceptionse mais de uma exceção tiver sido lançada coletivamente por uma ou mais tarefas;
  • Evite ter que salvar Taskapenas para verificar seu Task.Exception;
  • Propagar o status de cancelamento adequadamente ( Task.IsCanceled), como algo como isso não faria isso: Task t = Task.WhenAll(...); try { await t; } catch { throw t.Exception; }.

Eu juntei a seguinte extensão para isso:

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

Agora, o seguinte funciona da maneira que eu quero:

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
fonte
2
Resposta fantástica
rola
-3

Isso funciona para mim

private async Task WhenAllWithExceptions(params Task[] tasks)
{
    var result = await Task.WhenAll(tasks);
    if (result.IsFaulted)
    {
                throw result.Exception;
    }
}
Alexey Kulikov
fonte
1
WhenAllnão é o mesmo que WhenAny. await Task.WhenAny(tasks)será concluído assim que qualquer tarefa for concluída. Portanto, se você tiver uma tarefa que é concluída imediatamente e é bem-sucedida e outra leva alguns segundos antes de lançar uma exceção, ela retornará imediatamente sem nenhum erro.
StriplingWarrior
Então linha de lance nunca será atingido aqui - WhenAll teria jogado a exceção
thab
-5

Em seu código, a primeira exceção é retornada por design, conforme explicado em http://blogs.msdn.com/b/pfxteam/archive/2011/09/28/task-exception-handling-in-net-4-5. aspx

Quanto à sua pergunta, você receberá a AggreateException se escrever um código como este:

try {
    var result = Task.WhenAll(DoLongThingAsyncEx1(), DoLongThingAsyncEx2()).Result; 
}
catch (Exception ex) {
    // Expect AggregateException here
} 
Nebulosa
fonte