Получение значимой трассировки стека при использовании асинхронного кода
Я создал небольшую часть кода для параллельной работы нескольких асинхронных операций (сам класс Parallel
не подходит для асинхронных операций).
Он выглядит следующим образом:
public static async Task ForEachAsync<T>(this IEnumerable<T> source, int dop, Func<T, Task> body)
{
var chunks = source.Chunk(dop);
foreach (var chunk in chunks)
await Task.WhenAll(chunk.Select(async s => await body(s).ContinueWith(t => ThrowError(t))));
}
private static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunksize)
{
while (source.Any())
{
yield return source.Take(chunksize);
source = source.Skip(chunksize);
}
}
private static void ThrowError(Task t)
{
if (t.IsFaulted)
{
if (t.Exception.InnerExceptions != null && t.Exception.InnerExceptions.Count == 1)
throw t.Exception.InnerExceptions[0];
else
throw t.Exception;
}
}
Что касается запуска параллельных задач, приведенный выше код работает очень хорошо. Однако у меня есть некоторые проблемы при исключении исключений.
Код захвата исключений работает хорошо, пока возвращается сообщение об исключении, но трассировка стека оставляет желать многого - поскольку он указывает на метод ThrowError
, а не на метод, который изначально генерировал исключение. Я могу подобрать работу и выяснить, что пошло не так с приложенным отладчиком, но если я когда-нибудь выпущу это приложение, у меня не будет этой опции - в лучшем случае у меня будет исключение с протоколом стека, зарегистрированным.
Итак - есть ли способ получить более значимую трассировку стека при выполнении задач async?
PS. Это для приложения WindowsRT, но я думаю, что проблема не ограничивается WindowsRT как таковой...
Ответы
Ответ 1
Итак - есть ли способ получить более значимую трассировку стека при выполнении задач async?
Да, вы можете использовать ExceptionDispatchInfo.Capture
, который был введен в .NET 4.5 для асинхронного ожидания:
private static void ThrowError(Task t)
{
if (t.IsFaulted)
{
Exception exception =
t.Exception.InnerExceptions != null && t.Exception.InnerExceptions.Count == 1
? t.Exception.InnerExceptions[0]
: t.Exception;
ExceptionDispatchInfo.Capture(exception).Throw();
}
}
"Вы можете использовать объект ExceptionDispatchInfo
, который возвращается этим методом в другое время, и, возможно, в другом потоке, чтобы переустановить указанное исключение, как если бы исключение протекало с этой точки, где оно было захвачено до точки, где это восстает. Если исключение активно при его захвате, сохраняется текущая информация о трассировке стека и информация Watson, содержащаяся в исключении. Если он неактивен, то есть, если он не был сброшен, он не будет иметь никакой информации трассировки стека или информации Уотсона."
Однако имейте в виду, что исключения из асинхронного кода обычно менее значимы, чем вы хотели бы, поскольку все исключения выбрасываются из метода MoveNext
на конечной машине, сгенерированной компилятором.
Ответ 2
Ответ i3arnon является полностью правильным, однако есть еще несколько альтернатив. Проблема, с которой вы сталкиваетесь, состоит в том, что throw
- это та часть, которая захватывает трассировку стека, - снова бросая одно и то же исключение, вы выбрали всю трассировку стека.
Самый простой способ избежать этого - позволить Task
выполнить свою работу:
t.GetAwaiter().GetResult();
То, что это - исключение, вызывается с правильной трассировкой стека и всем. Вам даже не нужно проверять, не сбита ли задача - если она есть, она будет бросать, если нет, это не будет.
Внутри этого метода используется ExceptionDispatchInfo.Capture(exception).Throw();
i3arnon, поэтому они почти равны (ваш код предполагает, что задача уже завершена, сбой или нет - если она еще не завершена, IsFaulted
вернет false).
Ответ 3
приведенный выше код работает очень хорошо
Я не уверен, почему вы хотите, чтобы исключения были выбраны непосредственно в пуле потоков (ContinueWith
). Это приведет к сбою вашего процесса, не предоставив никому из кода возможность очистки.
Для меня гораздо более естественный подход состоял бы в том, чтобы исключить появление пузыря. В дополнение к естественной очистке этот подход устраняет все неудобные, странные коды:
public static async Task ForEachAsync<T>(this IEnumerable<T> source, int dop, Func<T, Task> body)
{
var chunks = source.Chunk(dop);
foreach (var chunk in chunks)
await Task.WhenAll(chunk.Select(s => body(s)));
}
private static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunksize)
{
while (source.Any())
{
yield return source.Take(chunksize);
source = source.Skip(chunksize);
}
}
// Note: No "ThrowError" at all.