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

В этом коде:

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?


person Michael Ray Lovett    schedule 17.08.2012    source источник
comment
WhenAll делает создание файла AggregateException. Если бы вы использовали Task.Wait вместо await в своем примере, вы бы поймали AggregateException   -  person Peter Ritchie    schedule 17.08.2012
comment
+1, это то, что я пытаюсь понять, сэкономьте мне часы отладки и гугления.   -  person kennyzx    schedule 23.10.2012
comment
Впервые за несколько лет мне понадобились все исключения из Task.WhenAll, и я попал в ту же ловушку. Поэтому я попытался вдаваться в подробности об этом поведении.   -  person noseratio    schedule 27.06.2020
comment
comment
@PeterRitchie Это правда, но имейте в виду, что Task.Wait блокирует, а await нет.   -  person xr280xr    schedule 27.03.2021


Ответы (8)


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

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

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

-- Изменить --

Понятно:

Async Primer by Bill Вагнер

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

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

person decyclone    schedule 17.08.2012
comment
Да, я знаю, что были внесены некоторые изменения в обработку исключений, но новейшие документы для состояния Task.WhenAll агрегация набора развернутых исключений из каждой из предоставленных задач.... В моем случае обе мои задачи завершаются в состоянии сбоя... - person Michael Ray Lovett; 17.08.2012
comment
@MichaelRayLovett: вы нигде не храните возвращенную задачу. Бьюсь об заклад, когда вы посмотрите на свойство Exception этой задачи, вы получите AggregateException. Но в вашем коде вы используете await. Это делает AggregateException развернутым в фактическое исключение. - person decyclone; 17.08.2012
comment
Я тоже думал об этом, но возникли две проблемы: 1) я не могу понять, как сохранить задачу, чтобы я мог ее изучить (т.е. Task myTask = await Task.WhenAll(...) не кажется, работает. и 2) я думаю, я не понимаю, как await может когда-либо представлять несколько исключений как одно исключение ... о каком исключении он должен сообщать? Выбрать наугад? - person Michael Ray Lovett; 17.08.2012
comment
Задача myTask = Задача.КогдаВсе(...); ждать мою задачу; - person decyclone; 17.08.2012
comment
Да, когда я сохраняю задачу и проверяю ее в try/catch ожидания, я вижу, что это исключение — AggregatedException. Итак, документы, которые я прочитал, верны; Task.WhenAll упаковывает исключения в AggregateException. Но затем await разворачивает их. Сейчас я читаю вашу статью, но пока не понимаю, как await может выбрать одно исключение из AggregateExceptions и сгенерировать его вместо другого. - person Michael Ray Lovett; 17.08.2012
comment
Прочтите статью, спасибо. Но я до сих пор не понимаю, почему await представляет AggregateException (представляющее несколько исключений) как одно единственное исключение. Как это всесторонняя обработка исключений? .. Я думаю, если я хочу точно знать, какие задачи вызывали исключения, а какие они вызывали, мне нужно было бы изучить объект Task, созданный Task.WhenAll?? - person Michael Ray Lovett; 17.08.2012
comment
Да, вам нужно будет изучить возвращенную задачу. Проще говоря, в коде, который не использует async/await, сколько исключений вы обрабатываете одновременно? Ответ, наверное, один. Обычно это последнее исключение, которое было сгенерировано и не обработано. С ожиданием они, кажется, создают точно такой же сценарий. Если вам нужен расширенный контроль, вы всегда можете проверить свойство Exception возвращаемой задачи. - person decyclone; 17.08.2012
comment
Спасибо, теперь понял. В этой статье все ясно изложено: blogs.msdn. com/b/pfxteam/archive/2011/09/28/10217876.aspx - person Michael Ray Lovett; 17.08.2012
comment
Я думаю, что это хороший ответ, когда await используется для одной задачи async. Однако в случае, когда используется WhenAll, кажется, лучше получать агрегированные исключения для каждой невыполненной задачи, что невозможно при использовании await с WhenAll (в .NET Core 3.1), если только вы не поймаете исключение, созданное await и затем проверьте атрибут Exception задачи, возвращенной из WhenAll. - person Toby Artisan; 02.03.2020
comment
Что происходит, когда у вас возникает несколько исключений в нескольких задачах внутри Task.WhenAll? Как он мог выдать что-либо, кроме AggregateException? Если бы это было не так, как бы он узнал, какое исключение бросить? Только первый? - person rolls; 16.06.2020
comment
@rolls, проверьте мой ответ, который охватывает эту и некоторые другие тонкости. - person noseratio; 29.06.2020
comment
Вот PDF-файл, который можно прочитать в браузере. - person c24w; 23.07.2020

Я знаю, что на этот вопрос уже был дан ответ, но выбранный ответ на самом деле не решает проблему 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 (даже если только одна задача вызвала исключение).

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

person Richiban    schedule 24.08.2016
comment
Отличный четкий ответ, это должно быть выбрано ИМО. - person bytedev; 22.02.2018
comment
+1, а нельзя ли просто поместить throw task.Exception; в блок catch? (Меня смущает то, что я вижу пустой перехват, когда на самом деле обрабатываются исключения.) - person AnorZaken; 06.11.2019
comment
@AnorZaken Абсолютно; Я не помню, почему изначально написал это так, но я не вижу никаких недостатков, поэтому я переместил его в блок catch. Спасибо - person Richiban; 23.12.2019
comment
Одним из незначительных недостатков этого подхода является то, что статус отмены (Task.IsCanceled) не распространяется должным образом. Это можно решить с помощью помощника расширения, такого как this. - person noseratio; 29.06.2020

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

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(); });
}
person jgauffin    schedule 29.01.2016
comment
это не работает. WhenAll завершает работу при первом исключении и возвращает его. см.: stackoverflow.com/questions/6123406/waitall-vs-whenall - person jenson-button-event; 23.02.2018
comment
Предыдущие два комментария неверны. Код действительно работает, и exceptions содержит оба исключения. - person Tobias; 11.05.2018
comment
DoLongThingAsyncEx2() должен вызывать новое исключение InvalidOperationException() вместо нового InvalidOperation(). - person Artemious; 09.12.2018
comment
Чтобы развеять любые сомнения, я собрал расширенную скрипту, которая, надеюсь, точно покажет, как работает эта обработка: dotnetfiddle.net/X2AOvM< /а>. Вы можете видеть, что await вызывает развертывание первого исключения, но все исключения действительно по-прежнему доступны через массив задач. - person nuclearpidgeon; 02.01.2019

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

Проблема

Ожидание 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));
}

Решение (TLDR)

So, back to await Task.WhenAll(...), what I personally wanted is to be able to:

  • Получить одно исключение, если было выброшено только одно;
  • Получите 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}");
}
person noseratio    schedule 27.06.2020
comment
Фантастический ответ - person rolls; 01.07.2020
comment
Небольшое замечание относительно реализации метода WithAggregatedExceptions: насколько мне известно, условие anteTask.Exception is AggregateException ex всегда будет успешным, поэтому оно просто служит для присвоения anteTask.Exception переменной ex. - person Theodor Zoulias; 14.02.2021
comment
@TheodorZoulias, думал! Я просто хотел, чтобы одна строчка представила ex :) - person noseratio; 14.02.2021
comment
Еще одно замечание: идея распространения AggregatedException только в том случае, если задача хранит внутри более одного исключения, проблематична. Представьте, например, что вы ожидаете Task.WhenAll, который может выйти из строя на один или несколько FileNotFoundException. В этом случае вам придется дублировать логику обработки ошибок, имея как catch (FileNotFoundException), так и catch (AggregateException aex), которые фильтруют FileNotFoundException. И одинаковая логика обработки должна быть на обоих обработчиках. Лично мне проще распространять AggregatedException во всех случаях. - person Theodor Zoulias; 15.02.2021
comment
Хотя отдельные исключения в специальном регистре имеют смысл, если это исключение OperationCanceledException. Распространение AggregateException, содержащего OperationCanceledException, неудобно. - person Theodor Zoulias; 15.02.2021
comment
@TheodorZoulias, я вижу, ты намекаешь. Мне еще предстоит столкнуться с этим сценарием, но в этом случае можно использовать упрощенную версию (нет необходимости в проверке ex.InnerExceptions.Count > 1 || ex.InnerException is AggregateException), она всегда будет давать развернутый AggregateException. - person noseratio; 15.02.2021
comment
Однако в большинстве случаев (и я согласен здесь со Стивеном Тусбом и Стивеном Клири) нас интересует только факт исключения, и первого достаточно. Лучшее, что мы можем сделать, это войти в систему и, возможно, повторить попытку. И здесь мое расширение более полезно, я считаю. Он предоставляет агрегированную информацию о диагностике при обработке общего Exception, например: .net/ - person noseratio; 15.02.2021
comment
А что касается OperationCanceledException, это не только само исключение, но и статус Task.IsCanceled, который правильно распространяется этим расширением. - person noseratio; 15.02.2021
comment
Я не думаю, что Task.IsCanceled будет правильно распространяться при использовании try { await source.ConfigureAwait(false); } catch { source.Wait(); }, например этого. Думаю это станет Task.IsFaulted. Возможно, это не большая проблема, но на нее стоит обратить внимание. - person noseratio; 15.02.2021
comment
Да, вы правы, что IsCanceled не сохранился, что немного грустно. Насколько я знаю, асинхронные методы никогда не могут создать задачу, имеющую (в конечном итоге) отмененное состояние. Это делает ваш подход ContinueWith+Wrap лучше, я должен сказать. Я бы хотел, чтобы был атрибут или что-то, что могло бы настроить асинхронный конечный автомат, чтобы сделать возможной отправку отмененных задач. Кстати, вы заметили, насколько плох Message сплющенного AggregateException? Каждое внутреннее сообщение повторяется дважды. Мой обходной путь - сделать это: new AggregateException(ex.Flatten().InnerExceptions) - person Theodor Zoulias; 15.02.2021
comment
@TheodorZoulias, да, повторяющиеся сообщения - это плохо ... но лучше, чем произошла одна или несколько ошибок :) Я обновлю ответ вашим предложением по поводу ex.Flatten().InnerExceptions, если вы не возражаете. - person noseratio; 15.02.2021
comment
Кстати, насколько я знаю, асинхронные методы никогда не могут создать задачу, имеющую (в конечном итоге) отмененное состояние - не уверен, что я следую, но этот делает: async Task TestAsync() { await Task.FromException(new TaskCanceledException()); }. Здесь Task.IsCanceled будет true, как если бы мы только что сделали throw new TaskCanceledException() внутри async method. - person noseratio; 15.02.2021
comment
Да, вы правы. Я только что проверил это, и IsCanceled сохраняется. И с правильным CancellationToken. У меня было совершенно неправильное мнение. Кстати, да, конечно, вы можете отредактировать вопрос с этим предложением! - person Theodor Zoulias; 15.02.2021

Просто подумал, что я бы расширил ответ @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");
}
person Daniel Šmon    schedule 02.04.2018

Вы думаете о Task.WaitAll — выдает AggregateException.

WhenAll просто выдает первое исключение из списка исключений, с которыми он сталкивается.

person Mohit Datta    schedule 17.04.2015
comment
Это неправильно, задача, возвращенная из метода WhenAll, имеет свойство Exception, которое является AggregateException, содержащим все исключения, созданные в его InnerExceptions. Здесь происходит то, что await выдает первое внутреннее исключение вместо самого AggregateException (как сказал дециклон). Вызов метода Wait задачи вместо его ожидания приводит к возникновению исходного исключения. - person Şafak Gür; 21.02.2018
comment
На самом деле и этот ответ, и предыдущий комментатор точны. await на WhenAll развернет совокупное исключение и передаст первое исключение в списке в перехват. Для исходного вопроса, чтобы получить совокупное исключение в блоке catch, как и ожидалось, следует использовать Task.WaitAll - person Theo; 03.12.2020

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

private async Task WhenAllWithExceptions(params Task[] tasks)
{
    var result = await Task.WhenAll(tasks);
    if (result.IsFaulted)
    {
                throw result.Exception;
    }
}
person Alexey Kulikov    schedule 14.12.2018
comment
WhenAll не то же самое, что WhenAny. await Task.WhenAny(tasks) завершится, как только будет завершена любая задача. Таким образом, если у вас есть одна задача, которая завершается немедленно и успешно, а другая занимает несколько секунд, прежде чем выдать исключение, она вернется немедленно без каких-либо ошибок. - person StriplingWarrior; 01.03.2019
comment
Тогда линия броска никогда не будет здесь затронута - WhenAll выдал бы исключение - person thab; 20.05.2019

В вашем коде первое исключение возвращается по дизайну, как описано в 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
} 
person Nebula    schedule 04.09.2014