Получить ВСЕ трассировки стека в приложении async / await - PullRequest
1 голос
/ 02 августа 2020

Я хочу получить информацию обо всех стеках вызовов (или получить все трассировки стека) в моем асинхронном C# приложении. Я знаю, , как получить трассировку стека всех существующих потоков .

Но как получить информацию обо всех стеках вызовов, выпущенных await , которые не имеют запущенного потока на нем?

ПРИМЕР КОНТЕКСТА

Предположим, что следующий код:

private static async Task Main()
{
    async Task DeadlockMethod(SemaphoreSlim lock1, SemaphoreSlim lock2)
    {
        await lock1.WaitAsync();
        await Task.Delay(500);
        await lock2.WaitAsync(); // this line causes the deadlock
    }

    SemaphoreSlim lockA = new SemaphoreSlim(1);
    SemaphoreSlim lockB = new SemaphoreSlim(1);

    Task call1 = Task.Run(() => DeadlockMethod(lockA, lockB));
    Task call2 = Task.Run(() => DeadlockMethod(lockB, lockA));

    Task waitTask = Task.Delay(1000);

    await Task.WhenAny(call1, call2, waitTask);

    if (!call1.IsCompleted
        && !call2.IsCompleted)
    {
        // DUMP STACKTRACES to find the deadlock
    }
}

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

Если строку await lock2.WaitAsync(); изменить на lock2.Wait();, то уже упомянутым можно будет получить трассировки стека всех потоков . Но как вывести список всех трассировок стека без работающего потока?

ПРЕДОТВРАЩЕНИЕ НЕПРАВИЛЬНОГО ПОНЯТИЯ:

  • Пример очень упрощен, он просто иллюстрирует одну из потенциальных сложностей. Исходной проблемой является сложное многопоточное приложение, которое запускается на сервере, и может возникнуть множество трудных для исследования проблем, связанных с параллельными процессами. но и для других целей. Поэтому, пожалуйста, не советуйте мне, как избежать взаимоблокировок или как написать многопоточное приложение - это не суть вопроса.
  • Вы можете ответить на это в целом, но также и решение, работающее по крайней мере. Net Core 3.1 хватит.

Ответы [ 2 ]

4 голосов
/ 02 августа 2020

Я знаю, как получить трассировку стека всех существующих потоков.

Просто расскажу немного об истории.

В Windows потоки являются Концепция ОС. Они единица планирования. Итак, где-то есть определенный список потоков, поскольку это то, что использует планировщик ОС.

Более того, каждый поток имеет стек вызовов. Это восходит к первым дням компьютерного программирования. Однако назначение стека вызовов часто понимается неправильно. Стек вызовов используется как последовательность мест возврата. Когда метод возвращается, он выталкивает аргументы своего стека вызовов из стека, а также место возврата, а затем переходит к месту возврата. код попал в ситуацию; он представляет где код возвращается из текущего метода . Стек вызовов - это то место, где код переходит в , а не то место, откуда он пришел из . Вот почему существует стек вызовов: чтобы направлять будущий код, а не помогать диагностике. Теперь выясняется, что в стеке вызовов действительно есть полезная информация для диагностики, так как он указывает, откуда взялся код , а также , куда он направляется, поэтому стеки вызовов находятся в исключениях. и обычно используются для диагностики. Но это не настоящая причина, по которой существует стек вызовов; это просто счастливое обстоятельство.

Теперь введите асинхронный код.

В асинхронном коде стек вызовов по-прежнему представляет, куда возвращается код (как и все стеки вызовов). Но в асинхронном коде стек вызовов больше не представляет , где код пришел из . В синхронном мире эти две вещи были одинаковыми, и стек вызовов (который необходим) также можно использовать для ответа на вопрос «как этот код попал сюда?». В асинхронном мире стек вызовов по-прежнему необходим, но отвечает только на вопрос «куда идет этот код?» и не может ответить на вопрос «как сюда попал этот код?». Чтобы ответить на вопрос "как этот код попал сюда?" вопрос вам нужна причинно-следственная цепочка .

Более того, для правильной работы необходимы стеки вызовов (как в синхронном, так и в асинхронном мире), и поэтому компилятор / среда выполнения гарантирует их существование. Цепочки причинно-следственной связи не необходимы, и они не предоставляются из коробки. В синхронном мире стек вызовов представляет собой цепочку причинно-следственных связей, что приятно, но это счастливое обстоятельство не переносится в асинхронный мир.

Когда поток освобождается с помощью await , трассировка стека и все объекты в стеке вызовов где-то хранятся.

Нет; это не вариант. Это было бы верно, если бы async использовало волокна, но это не так. Стек вызовов нигде не сохраняется.

Потому что в противном случае поток продолжения потеряет контекст.

Когда await возобновляется, ему нужен только достаточный контекст для продолжения выполнения свой собственный метод и потенциально завершающий метод. Итак, имеется структура конечного автомата async, которая помещена в кучу; эта структура содержит ссылки на локальные переменные (включая this и аргументы метода). Но это все, что нужно для корректности программы; стек вызовов не нужен, поэтому он не сохраняется.

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

На уровне реализации async / await больше похоже на обратные вызовы, чем что-либо еще. Когда метод достигает await, он помещает свою структуру конечного автомата в кучу (если это еще не сделано) и подключает обратный вызов. Этот обратный вызов запускается (вызывается напрямую), когда задача завершается, и это продолжает выполнение метода async. Когда этот async метод завершается, он выполняет свои задачи, и все, что await в этих задачах, затем вызывается для продолжения выполнения. Итак, если вся последовательность задач завершена, вы фактически получите стек вызовов, который является инверсией стека причинности.

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

Итак, здесь есть пара проблем. Во-первых, не существует глобального списка всех Task объектов (или, в более общем смысле, объектов, подобных задачам). И это было бы трудно получить.

Во-вторых, для каждого асинхронного метода / задачи в любом случае нет причинно-следственной цепочки. Компилятор не генерирует его, потому что это не обязательно для правильной работы.

Нельзя сказать, что ни одна из этих проблем непреодолима - просто сложно. Я поработал над проблемой цепочки причинно-следственных связей с моей библиотекой AsyncDiagnostics . На данный момент он довольно старый, но его довольно легко обновить до. NET Core. Он использует PostSharp для изменения кода, созданного компилятором для каждого метода, и отслеживания цепочек причинно-следственной связи вручную. Получение списка всех типов задач и связывание причинно-следственных связей с каждым из них - еще одна проблема, которая, вероятно, потребует использования прикрепленного профилировщика. Мне известно о других компаниях, которые хотели это решение, но ни одна из них не уделила времени, необходимому для его создания; все они сочли более эффективными проводить проверку кода, аудит и обучение разработчиков.

0 голосов
/ 04 августа 2020

Я отметил ответ Стивена Клири как правильный ответ . Он намекал и подробно объяснил, почему это так сложно.

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

ВОЗМОЖНОЕ РЕШЕНИЕ ПРОБЛЕМЫ

Допущение: трассировки стека, включая собственный код, достаточно.

Исходя из предположения, мы можем сделать следующее:

  1. Инкапсулировать все вызываемые внешние методы Asyn c (отслеживать их вход и выход)
  2. Реализовать проверку стиля, которая предупредит об использовании любого метода Asyn c из пространств имен вашего проекта

Ad 1 .: Инкапсуляция

Предположим, внешний метод Task ExternalObject.ExternalAsync(). Мы создадим инкапсулирующий метод расширения:

public static async Task MyExternalAsync(this ExternalObject obj)
{
    using var disposable = AsyncStacktraces.MethodStarted();

    await obj.ExternalAsync();
}

Во время вызова AsyncStacktraces.MethodStarted(); stati c текущая трассировка стека будет записана из свойства Environment.StackTrace в некоторый словарь stati c вместе с disposable объект. Проблем с производительностью не будет, поскольку сам метод asyn c, скорее всего, намного дороже, чем извлечение stacktrace.

Объект disposable реализует интерфейс IDisposable. Метод .Dispose() удалит текущую трассировку стека из словаря c stati в конце метода MyExternalAsync().

Обычно в решении фактически вызываются только несколько десятков внешних методов Asyn c , поэтому усилия довольно низкие.

Ad 2 .: Проверка стиля

Расширение проверки пользовательского стиля будет предупреждать, когда кто-либо напрямую использует внешний метод Asyn c. CI можно настроить так, чтобы он не проходил при наличии этого предупреждения. В некоторых местах, где нам понадобится прямой внешний метод Asyn c, мы будем использовать #pragma warning disable.

...