Подтвердить что ты не робот

Получение значимой трассировки стека при использовании асинхронного кода

Я создал небольшую часть кода для параллельной работы нескольких асинхронных операций (сам класс 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 как таковой...

4b9b3361

Ответ 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.