AggregateException из Task.WhenAll содержит только первое исключение, когда ожидается - PullRequest
1 голос
/ 22 апреля 2020

При возникновении нескольких исключений в вызове Task.WhenAll создается впечатление, что только одно из исключений включается в задание, как только вы ожидаете его через более чем один уровень ожидания. У меня сложилось впечатление, что свойство Task.Exception.InnerExceptions будет содержать все возникшие исключения, но при определенных обстоятельствах они, похоже, имеют только одно.

Например, этот пример кода создает несколько задач, генерирующих исключения, и затем ожидает Task.WhenAll на них, а затем записывает в консоль исключения, которые он может перехватить:

class Program
{
    static async Task Main(string[] args)
    {
        var task = CauseMultipleExceptionsAsync();

        // Delaying until all the Exceptions have been thrown, ensuring it isn't just a weird race condition happening behind the scenes
        await Task.Delay(5000);

        try
        {
            await task;
        }
        catch(AggregateException e)
        {
            // This does not get hit
            Console.WriteLine($"AggregateException caught: Found {e.InnerExceptions.Count} inner exception(s)");
        }
        catch(Exception e)
        {
            Console.WriteLine($"Caught other Exception {e.Message}");

            Console.WriteLine($"task.Exception.InnerExceptions contains {task.Exception.InnerExceptions.Count} exception(s)");
            foreach (var exception in task.Exception.InnerExceptions)
            {
                Console.WriteLine($"Inner exception {exception.GetType()}, message: {exception.Message}");
            }
        }
    }

    static async Task CauseMultipleExceptionsAsync()
    {
        var tasks = new List<Task>()
        {
            CauseExceptionAsync("A"),
            CauseExceptionAsync("B"),
            CauseExceptionAsync("C"),
        };

        await Task.WhenAll(tasks);
    }

    static async Task CauseExceptionAsync(string message)
    {
        await Task.Delay(1000);
        Console.WriteLine($"Throwing exception {message}");
        throw new Exception(message);
    }
}

Я ожидал, что это либо введет предложение catch(AggregateException e), либо, по крайней мере, будет иметь три внутренних исключения в task.Exception.InnerExceptions - что на самом деле происходит, когда возникает одно единственное исключение, и только * одно исключение находится в task.Exception.InnerExceptions:

Throwing exception B
Throwing exception A
Throwing exception C
Caught other Exception A
task.Exception.InnerExceptions contains 1 exception(s)
Inner exception System.Exception, message: A

Что еще более странно, так это поведение меняется в зависимости от того, дождитесь вызова Task.WhenAll в CauseMultipleExceptionsAsync - если вы возвращаете задачу напрямую, а не ожидаете ее, то все три исключения появляются в task.Exception.InnerException. Например, замена CauseMultipleExceptionsAsync на это:

    static Task CauseMultipleExceptionsAsync()
    {
        var tasks = new List<Task>()
        {
            CauseExceptionAsync("A"),
            CauseExceptionAsync("B"),
            CauseExceptionAsync("C"),
        };

        return Task.WhenAll(tasks);
    }

Дает этот результат со всеми тремя исключениями, содержащимися в task.Exception.InnerExceptions:

Throwing exception C
Throwing exception A
Throwing exception B
Caught other Exception A
task.Exception.InnerExceptions contains 3 exception(s)
Inner exception System.Exception, message: A
Inner exception System.Exception, message: B
Inner exception System.Exception, message: C

Я очень запутался это - откуда появились исключения B и C go в исходном примере? Как бы вы go обнаружили их снова, если Task.Exception не содержит никакой информации о них? Почему ожидание внутри CauseMultipleExceptionsAsync скрывает эти исключения, а возврат Task.WhenAll напрямую не делает?

Если это имеет значение, я могу повторить вышеизложенное в обоих. Net Framework 4.5.2 и. Net Core 2.1.

1 Ответ

2 голосов
/ 22 апреля 2020

То, что вы наблюдаете, это поведение оператора await, а не поведение метода Task.WhenAll. Если вас интересует, почему await ведет себя так, вы можете прочитать эту статью с первых дней асинхронной / await:

Имея выбор всегда бросать первый или всегда выбрасывающий агрегат, для await мы выбираем всегда выбрасывать первый. Это не значит, однако, что у вас нет доступа к тем же деталям. Во всех случаях свойство Exception задачи по-прежнему возвращает AggregateException, которое содержит все исключения, так что вы можете перехватить любое выброшенное значение и go вернуться к Task.Exception при необходимости. Да, это приводит к несоответствию между поведением исключений при переключении между task.Wait() и await task, но мы рассматриваем это как значительное меньшее из двух зол.

В случае, если вы хотите реализовать метод, похожий по поведению на Task.WhenAll, но без потери удобства асинхронного / ожидающего механизма, это сложно, но есть обходные пути здесь .

...