Я хочу ждать, чтобы выбросить AggregateException, а не только первое исключение

При ожидании неудачной задачи (той, которая имеет набор исключений) await будет отменять сохраненное исключение. Если хранимое исключение - это AggregateException, оно отменяет первое и отменяет остальные.

Как мы можем использовать await и в то же время бросать исходный AggregateException, чтобы мы не случайно теряли информацию об ошибке?

Обратите внимание, что, конечно, можно подумать о взломанных решениях для этого (например, try-catch вокруг await, а затем вызвать Task.Wait). Я действительно хочу найти чистое решение. Что такое лучшая практика здесь?

Я думал об использовании пользовательского awaiter, но встроенный TaskAwaiter содержит много магии, что я не уверен, как полностью воспроизвести. Он вызывает внутренние API-интерфейсы типов TPL. Я также не хочу воспроизводить все это.

Вот короткое воспроизведение, если вы хотите сыграть с ним:

static void Main()
{
    Run().Wait();
}

static async Task Run()
{
    Task[] tasks = new[] { CreateTask("ex1"), CreateTask("ex2") };
    await Task.WhenAll(tasks);
}

static Task CreateTask(string message)
{
    return Task.Factory.StartNew(() => { throw new Exception(message); });
}

Только одно из двух исключений выбрано в Run.

Обратите внимание, что другие вопросы о переполнении стека не адресуют эту конкретную проблему. Будьте осторожны, предлагая дубликаты.

Ответ 1

Я не согласен с импликацией в заголовке вопроса, что await поведение является нежелательным. Это имеет смысл в подавляющем большинстве сценариев. В ситуации WhenAll как часто вам действительно нужно знать все подробности ошибки, а не только одну?

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

Тем не менее, вы можете получить желаемое поведение с помощью метода расширения:

public static async Task WithAggregateException(this Task source)
{
  try
  {
    await source.ConfigureAwait(false);
  }
  catch
  {
    // source.Exception may be null if the task was canceled.
    if (source.Exception == null)
      throw;

    // EDI preserves the original exception stack trace, if any.
    ExceptionDispatchInfo.Capture(source.Exception).Throw();
  }
}

Ответ 2

Я знаю, что опаздываю, но я нашел этот аккуратный маленький трюк, который делает то, что вы хотите. Поскольку полный набор исключений доступен с ожидаемой задачей, вызывая эту задачу "Ожидание" или "Результат" выдает общее исключение.

    static void Main(string[] args)
    {
        var task = Run();
        task.Wait();
    }
    public static async Task Run()
    {

        Task[] tasks = new[] { CreateTask("ex1"), CreateTask("ex2") };
        var compositeTask = Task.WhenAll(tasks);
        try
        {
            await compositeTask.ContinueWith((antecedant) => { }, TaskContinuationOptions.ExecuteSynchronously);
            compositeTask.Wait();
        }
        catch (AggregateException aex)
        {
            foreach (var ex in aex.InnerExceptions)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }

    static Task CreateTask(string message)
    {
        return Task.Factory.StartNew(() => { throw new Exception(message); });
    }

Ответ 3

Обработка исключений (параллельная библиотека задач)

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

возможно, вы хотите этого

Бог (Джон Скит) объясняет, что ожидает обработки исключений

(лично я уклоняюсь от ожидания, но это только мое предпочтение)

в ответ на комментарии (слишком долго для ответа на комментарий)

Затем используйте потоки в качестве отправной точки для аналогичного аргумента, так как лучшие методы там будут источником для этого.

Исключения счастливо проглатываются, если вы не реализуете код, чтобы передать их (например, шаблон асинхронизации, который ожидание предварительно обертывает... вы добавляете их в объект args события, когда вы поднимаете событие). Когда у вас есть сценарий, в котором вы запускаете произвольное количество потоков и выполняете их, вы не имеете контроля над порядком или точкой, в которой вы завершаете каждый поток. Более того, вы никогда не будете использовать этот шаблон, если ошибка на одном была релевантна другой. Поэтому вы сильно подразумеваете, что выполнение остального полностью независимо - IE вы сильно подразумеваете, что исключения из этих потоков уже обрабатываются как исключения. Если вы хотите сделать что-то помимо обработки исключений в этих потоках в потоках, в которых они происходят (это bizzarre), вы должны добавить их в коллекцию блокировки, которая передается по ссылке - вы больше не рассматриваете исключения как исключения, а как часть информации - используйте параллельный пакет, оберните исключение в информацию, необходимую для определения контекста, из которого он пришел, - который был бы передан в нее.

Не сдерживайте свои варианты использования.

Ответ 4

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

public static async Task NoSwallow<TException>(this Task task) where TException : Exception {
    try {
        await task;
    } catch (TException) {
        var unexpectedEx = task.Exception
                               .Flatten()
                               .InnerExceptions
                               .FirstOrDefault(ex => !(ex is TException));
        if (unexpectedEx != null) {
            throw new NotImplementedException(null, unexpectedEx);
        } else {
            throw task.Exception;
        }
    }
}

Код потребления может выглядеть следующим образом:

try {
    await Task.WhenAll(tasks).NoSwallow<MyException>();
catch (AggregateException ex) {
    HandleExceptions(ex);
}

Исключение с MyException на MyException будет иметь тот же эффект, что и в синхронном мире, даже если оно случайно выбрасывается одновременно с MyException. NotImplementedException с NotImplementedException помогает не потерять исходную трассировку стека.

Ответ 5

Я не знаю, позволю ли мне обобщить два лучших (на мой взгляд) решения проблемы, представленной в этом очень интересном вопросе, ответы г-на Стивена Клири и г-на Адонти. Я немного изменил API, добавив параметр preserveAggregate, пытаясь соответствовать единственному встроенному параметру конфигурации, ConfigureAwait. Эти конфигурации являются цепными, например:

await Task.WhenAll(tasks).ConfigureException(true).ConfigureAwait(false);

Вот версия мистера Стивена Клири (слегка измененная):

public static class TaskConfigurationExtensions
{
    public static async Task ConfigureException(this Task task, bool preserveAggregate)
    {
        try
        {
            await task.ConfigureAwait(false); // Because the context doesn't have to be resumed on to throw.
        }
        catch
        {
            if (preserveAggregate) throw task.Exception;
            throw;
        }
    }
    public static async Task<T> ConfigureException<T>(this Task<T> task, bool preserveAggregate)
    {
        try
        {
            return await task.ConfigureAwait(false);
        }
        catch
        {
            if (preserveAggregate) throw task.Exception;
            throw;
        }
    }
}

А вот версия мистера Адонти (также слегка измененная):

public static class TaskConfigurationExtensions
{
    private static void Empty<T>(T value) { }
    public static async Task ConfigureException(this Task task, bool preserveAggregate)
    {
        if (preserveAggregate)
        {
            await task
                .ContinueWith(Empty, TaskContinuationOptions.ExecuteSynchronously)
                .ConfigureAwait(false);
            task.Wait();
            return;
        }
        await task.ConfigureAwait(false);
    }
    public static async Task<T> ConfigureException<T>(this Task<T> task, bool preserveAggregate)
    {
        if (preserveAggregate)
        {
            await task
                .ContinueWith(Empty, TaskContinuationOptions.ExecuteSynchronously)
                .ConfigureAwait(false);
            return task.Result;
        }
        return await task.ConfigureAwait(false);
    }
}

Насколько я могу судить, обе реализации эквивалентны и превосходны.