Захват агрегированного исключения

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

Мой код:

var numbers = Enumerable.Range(0, 20);

try
{
    var parallelResult = numbers.AsParallel()
        .Where(i => IsEven(i));
    parallelResult.ForAll(e => Console.WriteLine(e));

}
catch (AggregateException e)
{
    Console.WriteLine("There was {0} exceptions", e.InnerExceptions.Count());
}

Он вызывает функцию IsEven

private static bool IsEven(int i)
{
    if (i % 10 == 0)
        throw new AggregateException("i");
    return i % 2 == 0;
}

Это генерирует исключение AggregateException.

Я бы ожидал, что код будет записывать каждое четное число в диапазоне 0,20 и "Было 1 исключение" дважды.

То, что я получаю, - это напечатанные некоторые цифры (они являются случайной причиной ForAll), а затем исключение генерируется, но не вылавливается и программы останавливаются.

Мне что-то не хватает?

Ответ 1

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

Вся точка AggregateException состоит в объединении нескольких исключений, которые могут возникать одновременно (или почти так) в параллельном процессе. Таким образом, ожидается, что AggregateException будет иметь как минимум одно внутреннее исключение. Но вы бросаете new AggregateException("i"), у которого нет внутренних исключений. Код PLINQ пытается проверить свойство InnerExceptions, наносит какую-то ошибку (возможно, NullPointerException), а затем, похоже, переходит в какой-то цикл. Это, возможно, ошибка в PLINQ, так как вы используете допустимый конструктор для AggregateException, даже если он необычный.

Как указано в другом месте, бросание ArgumentException было бы более семантически правильным. Но вы можете получить поведение, которое вы ищете, выбрасывая правильно построенный AggregateException, например, изменив функцию IsEven на что-то вроде этого:

private static bool IsEven(int i)
{
    if (i % 10 == 0){
        //This is still weird
        //You shouldn't do this. Just throw the ArgumentException.
        throw new AggregateException(new ArgumentException("I hate multiples of 10"));
    }
    return i % 2 == 0;
}

Я думаю, что мораль этой истории состоит в том, чтобы не бросать AggregateException, если вы точно не знаете, что делаете, особенно если вы уже находитесь в параллельной или Task -одной операции.

Ответ 2

Я согласен с другими: это ошибка в .Net, и вы должны сообщить об этом.

Причина заключается в методе QueryEnd() во внутреннем классе QueryTaskGroupState. Его декомпилированный (и слегка модифицированный для ясности) код выглядит следующим образом:

try
{
  this.m_rootTask.Wait();
}
catch (AggregateException ex)
{
  AggregateException aggregateException = ex.Flatten();
  bool cacellation = true;
  for (int i = 0; i < aggregateException.InnerExceptions.Count; ++i)
  {
    var canceledException =
        aggregateException.InnerExceptions[i] as OperationCanceledException;
    if (IsCancellation(canceledException))
    {
      cacellation = false;
      break;
    }
  }
  if (!cacellation)
    throw aggregateException;
}
finally
{
  this.m_rootTask.Dispose();
}
if (!this.m_cancellationState.MergedCancellationToken.IsCancellationRequested)
  return;
if (!this.m_cancellationState.TopLevelDisposedFlag.Value)
  CancellationState.ThrowWithStandardMessageIfCanceled(
    this.m_cancellationState.ExternalCancellationToken);
if (!userInitiatedDispose)
  throw new ObjectDisposedException(
    "enumerator", "The query enumerator has been disposed.");

В основном, что это такое:

  • сбросить сплющенный AggregateException, если он содержит исключения без отмены
  • введите новое исключение отмены, если была запрошена отмена (или возврат без металирования, я действительно не понимаю эту часть, но я не думаю, что это имеет значение здесь).
  • else throw ObjectDisposedException по какой-либо причине (предполагается, что userInitiatedDispose есть false, что есть)

Итак, если вы выбросите AggregateException без внутренних исключений, ex будет AggregateException, содержащим ваш пустой AggregateExcaption. Вызов Flatten() превратит это в пустую AggreateException, что означает, что она не содержит исключения исключений, поэтому первая часть кода считает, что это отмена и не выбрасывает.

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