Чому не чекає на Task.WhenAll кидає AggregateException?


102

У цьому коді:

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, це те, що я намагаюся зрозуміти, заощаджуючи години налагодження та гугл-інгу.
kennyzx

Вперше за чимало років мені потрібні були всі винятки Task.WhenAll, і я потрапив у ту ж пастку. Тож я намагався вдатися в глибокі подробиці цієї поведінки.
noseratio

Відповіді:


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. Але у своєму коді ви використовуєте await. Це робить AggregateException розгорнутим до фактичного винятку.
дециклон

3
Я теж про це думав, але виникли дві проблеми: 1) Здається, я не можу зрозуміти, як зберігати завдання, щоб я міг його вивчити (тобто "Завдання myTask = очікує Task.WhenAll (...)" не Здається, це працює. і 2) Я думаю, я не бачу, як await може представляти кілька винятків як лише один виняток .. про який виняток він повинен повідомляти? Виберіть один навмання?
Майкл Рей Ловетт,

2
Так, коли я зберігаю завдання та вивчаю його у спробі / лові await, я бачу, що це виняток - AggregatedException. Тож документи, які я читав, є правильними; Завдання.WhenAll обертає винятки в AggregateException. Але тоді await розгортає їх. Зараз я читаю вашу статтю, але я ще не розумію, як await може вибрати єдиний виняток із AggregateExceptions і перекинути цей проти іншого.
Майкл Рей Ловетт,

3
Прочитайте статтю, дякую. Але я все ще не розумію, чому await представляє AggregateException (представляє кілька винятків) як лише один виняток. Як це всебічне опрацювання винятків? .. Я думаю, якщо я хочу точно знати, які завдання кидали винятки, а які кидали, мені довелося б дослідити об'єкт Task, створений Task.WhenAll ??
Майкл Рей Ловетт,

55

Я знаю , що це питання , який вже відповів , але обраний відповідь не реально вирішити проблему Ора, тому я думав , що цей пост.

Це рішення надає вам сукупний виняток (тобто всі винятки, викликані різними завданнями), і не блокує (робочий процес все ще асинхронний).

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 (навіть якщо лише одне завдання викликало виняток).

Сподіваюся, це все ще корисно. Я знаю, що сьогодні у мене була ця проблема.


Відмінна чітка відповідь, це має бути обрана IMO.
bytedev

3
+1, але ви не можете просто помістити throw task.Exception;всередину catchблоку? (Мене бентежить, коли я бачу порожній улов, коли насправді обробляються винятки.)
AnorZaken

@AnorZaken Абсолютно; Я не пам’ятаю, чому я написав це так спочатку, але я не бачу жодного мінусу, тому перемістив його в блок catch. Подяка
Річібан

Незначним недоліком цього підходу є те, що статус скасування ( Task.IsCanceled) не розповсюджується належним чином. Це може бути вирішено за допомогою допоміжного розширення, як це .
noseratio

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
Дженсона-кнопковий подія

14
Попередні два коментарі є неправильними. Насправді код працює і exceptionsмістить обидва винятки.
Тобіас

DoLongThingAsyncEx2 () повинен викинути новий InvalidOperationException () замість нового InvalidOperation ()
Artemious

8
Щоб усунути будь-які сумніви тут, я зібрав розширену скрипту, яка, сподіваємось, показує, як саме відбувається ця обробка: 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методу завдання, а не очікування, спричиняє виникнення вихідного винятку.
faafak Gür

3

Тут є багато хороших відповідей, але я все одно хотів би опублікувати свою заяву, оскільки я щойно стикався з тією ж проблемою та проводив деякі дослідження. Або перейдіть до версії 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 :

Я намагався сказати, що це було глибоко обговорено багато років тому, коли вони спочатку були додані. Спочатку ми зробили те, що ви пропонуєте, із завданням, повернутим із WhenAll, що містить один AggregateException, який містив усі винятки, тобто task.Exception повертає оболонку AggregateException, яка містить інший AggregateException, який потім містить фактичні винятки; тоді, коли його очікували, внутрішній AggregateException буде розповсюджений. Вагомий відгук, який ми отримали, що змусив нас змінити дизайн, полягав у тому, що а) переважна більшість таких випадків мала досить однорідні винятки, наприклад, поширення всього в сукупності було не так важливо, б) розповсюдження сукупності потім порушило очікування щодо вилову для конкретних типів винятків, і в) для випадків, коли хтось хотів сукупність, вони могли зробити це явно за допомогою двох рядків, як я писав. Ми також вели обговорення щодо того, якою може бути поведінка 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)

Отже, повертаючись до того 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 кинув би виняток
tb

-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
} 
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.