Почему не ожидает в Task.WhenAll выдает исключение AggregateException?

103

В этом коде:

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

Я ожидал WhenAllсоздать и выбросить AggregateException, так как по крайней мере одна из задач, которую он ожидал, вызвала исключение. Вместо этого я получаю одно исключение, созданное одной из задач.

Не WhenAllвсегда создает AggregateException?

Майкл Рэй Ловетт
источник
7
WhenAll это создать AggregateException. Если бы вы использовали Task.Waitвместо awaitв своем примере, вы бы поймалиAggregateException
Питер Ричи
2
+1, это то, что я пытаюсь понять, сэкономьте мне часы на отладку и поиск в Google.
kennyzx
Впервые за несколько лет мне понадобились все исключения из Task.WhenAll, и я попал в ту же ловушку. Итак, я попытался подробно рассказать об этом поведении.
Nosratio

Ответы:

76

Я точно не помню, где, но я где-то читал, что с новыми ключевыми словами async / await они разворачивают AggregateExceptionфактическое исключение.

Итак, в блоке catch вы получаете фактическое исключение, а не агрегированное. Это помогает нам писать более естественный и интуитивно понятный код.

Это также было необходимо для упрощения преобразования существующего кода в использование async / await, когда большая часть кода ожидает определенных исключений, а не агрегированных исключений.

-- Редактировать --

Понял:

Асинхронный учебник Билла Вагнера

Билл Вагнер сказал: (в « Когда случаются исключения» )

... Когда вы используете await, код, сгенерированный компилятором, разворачивает AggregateException и генерирует базовое исключение. Используя await, вы избегаете дополнительной работы по обработке типа AggregateException, используемого Task.Result, Task.Wait и другими методами Wait, определенными в классе Task. Это еще одна причина использовать await вместо базовых методов Task ....

дециклон
источник
3
Да, я знаю, что произошли некоторые изменения в обработке исключений, но в новейшей документации для Task.WhenAll говорится: «Если какая-либо из поставленных задач завершится в состоянии сбоя, возвращенная задача также будет завершена в состоянии сбоя, где исключения будут содержать агрегирование набора развернутых исключений из каждой из поставленных задач ".... В моем случае обе мои задачи завершаются в неисправном состоянии ...
Майкл Рэй Ловетт
4
@MichaelRayLovett: вы нигде не сохраняете возвращенную задачу. Бьюсь об заклад, когда вы посмотрите на свойство Exception этой задачи, вы получите AggregateException. Но в вашем коде вы используете ожидание. Это заставляет AggregateException разворачиваться в фактическое исключение.
дециклон
3
Я тоже подумал об этом, но возникли две проблемы: 1) Кажется, я не могу понять, как сохранить задачу, чтобы я мог ее изучить (например, «Task myTask = await Task.WhenAll (...)» не работает Кажется, не работает. и 2) Я думаю, я не понимаю, как await может когда-либо представлять несколько исключений как одно исключение ... о каком исключении он должен сообщать? Выбрать наугад?
Майкл Рэй Ловетт
2
Да, когда я сохраняю задачу и просматриваю ее в try / catch ожидания, я вижу, что это исключение AggregatedException. Итак, документы, которые я прочитал, верны; Task.WhenAll заключает исключения в AggregateException. Но затем await разворачивает их. Я сейчас читаю вашу статью, но пока не понимаю, как await может выбрать одно исключение из AggregateExceptions и бросить это одно вместо другого ..
Майкл Рэй Ловетт
3
Прочтите статью, спасибо. Но я до сих пор не понимаю, почему await представляет AggregateException (представляющее несколько исключений) как одно единственное исключение. Как это комплексная обработка исключений? .. Думаю, если я хочу точно знать, какие задачи вызвали исключения, а какие - выбросили, мне нужно было бы изучить объект Task, созданный Task.WhenAll ??
Майкл Рэй Ловетт
56

Я знаю, что это вопрос, на который уже дан ответ, но выбранный ответ на самом деле не решает проблему OP, поэтому я подумал, что опубликую это.

Это решение дает вам совокупное исключение (то есть все исключения, которые были вызваны различными задачами) и не блокируется (рабочий процесс по-прежнему асинхронный).

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

Ключ состоит в том, чтобы сохранить ссылку на совокупную задачу, прежде чем вы ее ожидаете, тогда вы можете получить доступ к ее свойству Exception, которое содержит ваше AggregateException (даже если только одна задача вызвала исключение).

Надеюсь, это все еще полезно. Я знаю, что сегодня у меня была эта проблема.

Ричибан
источник
Отличный четкий ответ, это должен быть выбран ИМО.
bytedev
3
+1, а нельзя ли просто поместить throw task.Exception;внутрь catchблока? (Меня смущает пустой улов, когда на самом деле обрабатываются исключения.)
AnorZaken
@AnorZaken Совершенно верно; Я не помню, почему я так написал это изначально, но я не вижу недостатков, поэтому я переместил его в блок catch. Спасибо
Richiban
Одним из незначительных недостатков этого подхода является то, что статус отмены ( Task.IsCanceled) не передается должным образом. Это может быть решает , использующие помощника расширения , как это .
Носератион
34

Вы можете просмотреть все задачи, чтобы увидеть, возникло ли исключение более чем в одной:

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
это не работает. WhenAllзавершает работу при первом исключении и возвращает его. см .: stackoverflow.com/questions/6123406/waitall-vs-whenall
jenson-button-event
14
Предыдущие два комментария неверны. Код действительно работает и exceptionsсодержит оба выданных исключения.
Тобиас
DoLongThingAsyncEx2 () должен генерировать новое исключение InvalidOperationException () вместо нового InvalidOperation ()
Artemious
9
Чтобы развеять здесь сомнения, я собрал расширенную скрипку, которая, надеюсь, показывает, как именно работает эта обработка: dotnetfiddle.net/X2AOvM . Вы можете видеть, что это awaitвызывает разворачивание первого исключения, но все исключения действительно по-прежнему доступны через массив задач.
nuclearpidgeon
13

Просто подумал, что я бы расширил ответ @ Richiban, чтобы сказать, что вы также можете обрабатывать AggregateException в блоке catch, ссылаясь на него из задачи. Например:

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

Вы думаете о Task.WaitAll- выкидывает AggregateException.

WhenAll просто выбрасывает первое исключение из списка обнаруженных исключений.

Мохит Датта
источник
3
Это неправильно, задача, возвращаемая WhenAllметодом, имеет Exceptionсвойство, AggregateExceptionсодержащее все исключения, созданные в его InnerExceptions. Здесь происходит то, что awaitвыбрасывается первое внутреннее исключение вместо самого AggregateExceptionсебя (как сказал дециклон). Вызов Waitметода задачи вместо его ожидания вызывает исходное исключение.
afak Gür
4

Здесь много хороших ответов, но я все же хотел бы опубликовать свою тираду, поскольку я только что столкнулся с той же проблемой и провел некоторое исследование. Или перейдите к версии TL; DR ниже.

Эта проблема

Ожидание того, что taskвозвращено, Task.WhenAllвызывает только первое исключение из AggregateExceptionсохраненного в task.Exception, даже если несколько задач дали сбой.

В настоящее время документы дляTask.WhenAll говорят:

Если какая-либо из предоставленных задач завершается в состоянии сбоя, возвращенная задача также будет завершена в состоянии сбоя, где ее исключения будут содержать агрегирование набора развернутых исключений из каждой из предоставленных задач.

Это правильно, но ничего не говорится о вышеупомянутом поведении «разворачивания» при ожидании возвращенной задачи.

Я полагаю, что в документации об этом не упоминается, потому что такое поведение не является специфическим дляTask.WhenAll .

Это просто Task.Exceptionтип, AggregateExceptionи для awaitпродолжений он всегда по замыслу разворачивается как первое внутреннее исключение. Это отлично подходит для большинства случаев, поскольку обычно Task.Exceptionсостоит только из одного внутреннего исключения. Но рассмотрим этот код:

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

Здесь экземпляр AggregateExceptionразворачивается в свое первое внутреннее исключение InvalidOperationExceptionточно так же, как мы могли бы это сделать Task.WhenAll. Мы могли бы не наблюдать, DivideByZeroExceptionесли бы не прошли task.Exception.InnerExceptionsнапрямую.

Стивен Туб из Microsoft объясняет причину такого поведения в связанной проблеме GitHub :

Я пытался подчеркнуть, что это было подробно обсуждено много лет назад, когда они были изначально добавлены. Первоначально мы сделали то, что вы предлагаете, с помощью Task, возвращенного из WhenAll, содержащего одно исключение AggregateException, содержащее все исключения, т.е. task.Exception вернет оболочку AggregateException, которая содержит еще одно исключение AggregateException, которое затем будет содержать фактические исключения; затем, когда его ждут, будет распространено внутреннее исключение AggregateException. Полученная нами убедительная обратная связь заставила нас изменить дизайн: а) в подавляющем большинстве таких случаев были достаточно однородные исключения, так что распространение всего в совокупности не было так важно, б) распространение совокупности затем нарушило ожидания относительно уловов для определенных типов исключений, и c) для случаев, когда кому-то действительно нужна совокупность, они могут сделать это явно с помощью двух строк, как я написал. У нас также были обширные дискуссии о том, каким может быть поведение await в отношении задач, содержащих несколько исключений, и именно здесь мы остановились.

Еще одна важная вещь, на которую следует обратить внимание, это неглубокое поведение при разворачивании. То есть, он только развернет первое исключение из AggregateException.InnerExceptionsи оставит его там, даже если это будет экземпляр другого AggregateException. Это может добавить еще один слой путаницы. Например, давайте изменимся WhenAllWrongтак:

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

Решение (TL; DR)

Итак, вернемся к тому await Task.WhenAll(...), что я лично хотел, так это иметь возможность:

  • Получить единственное исключение, если было выброшено только одно;
  • Получить, AggregateExceptionесли одна или несколько задач коллективно сгенерировали более одного исключения;
  • Избегайте необходимости сохранять Taskтолько для проверки Task.Exception;
  • Размножаются статус отмены правильно ( Task.IsCanceled), а что - то вроде этого не будет делать , что: Task t = Task.WhenAll(...); try { await t; } catch { throw t.Exception; }.

Я собрал для этого следующее расширение:

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

Теперь следующее работает так, как я хочу:

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
Фантастический ответ
катит
-3

Это работает для меня

private async Task WhenAllWithExceptions(params Task[] tasks)
{
    var result = await Task.WhenAll(tasks);
    if (result.IsFaulted)
    {
                throw result.Exception;
    }
}
Алексей Куликов
источник
1
WhenAllне то же самое, что WhenAny. await Task.WhenAny(tasks)будет завершено, как только любая задача будет завершена. Поэтому, если у вас есть одна задача, которая завершается немедленно и успешно, а другая занимает несколько секунд до выдачи исключения, это немедленно вернется без каких-либо ошибок.
StriplingWarrior
Тогда линия броска никогда не будет хит здесь - WhenAll бросили бы исключение
thab
-5

В вашем коде первое исключение возвращается конструктивно, как описано на http://blogs.msdn.com/b/pfxteam/archive/2011/09/28/task-exception-handling-in-net-4-5. aspx

Что касается вашего вопроса, вы получите исключение AggreateException, если напишете такой код:

try {
    var result = Task.WhenAll(DoLongThingAsyncEx1(), DoLongThingAsyncEx2()).Result; 
}
catch (Exception ex) {
    // Expect AggregateException here
} 
Туманность
источник