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