Я знаю, как получить трассировку стека всех существующих потоков.
Просто расскажу немного об истории.
В Windows потоки являются Концепция ОС. Они единица планирования. Итак, где-то есть определенный список потоков, поскольку это то, что использует планировщик ОС.
Более того, каждый поток имеет стек вызовов. Это восходит к первым дням компьютерного программирования. Однако назначение стека вызовов часто понимается неправильно. Стек вызовов используется как последовательность мест возврата. Когда метод возвращается, он выталкивает аргументы своего стека вызовов из стека, а также место возврата, а затем переходит к месту возврата. код попал в ситуацию; он представляет где код возвращается из текущего метода . Стек вызовов - это то место, где код переходит в , а не то место, откуда он пришел из . Вот почему существует стек вызовов: чтобы направлять будущий код, а не помогать диагностике. Теперь выясняется, что в стеке вызовов действительно есть полезная информация для диагностики, так как он указывает, откуда взялся код , а также , куда он направляется, поэтому стеки вызовов находятся в исключениях. и обычно используются для диагностики. Но это не настоящая причина, по которой существует стек вызовов; это просто счастливое обстоятельство.
Теперь введите асинхронный код.
В асинхронном коде стек вызовов по-прежнему представляет, куда возвращается код (как и все стеки вызовов). Но в асинхронном коде стек вызовов больше не представляет , где код пришел из . В синхронном мире эти две вещи были одинаковыми, и стек вызовов (который необходим) также можно использовать для ответа на вопрос «как этот код попал сюда?». В асинхронном мире стек вызовов по-прежнему необходим, но отвечает только на вопрос «куда идет этот код?» и не может ответить на вопрос «как сюда попал этот код?». Чтобы ответить на вопрос "как этот код попал сюда?" вопрос вам нужна причинно-следственная цепочка .
Более того, для правильной работы необходимы стеки вызовов (как в синхронном, так и в асинхронном мире), и поэтому компилятор / среда выполнения гарантирует их существование. Цепочки причинно-следственной связи не необходимы, и они не предоставляются из коробки. В синхронном мире стек вызовов представляет собой цепочку причинно-следственных связей, что приятно, но это счастливое обстоятельство не переносится в асинхронный мир.
Когда поток освобождается с помощью await , трассировка стека и все объекты в стеке вызовов где-то хранятся.
Нет; это не вариант. Это было бы верно, если бы async
использовало волокна, но это не так. Стек вызовов нигде не сохраняется.
Потому что в противном случае поток продолжения потеряет контекст.
Когда await
возобновляется, ему нужен только достаточный контекст для продолжения выполнения свой собственный метод и потенциально завершающий метод. Итак, имеется структура конечного автомата async
, которая помещена в кучу; эта структура содержит ссылки на локальные переменные (включая this
и аргументы метода). Но это все, что нужно для корректности программы; стек вызовов не нужен, поэтому он не сохраняется.
Вы можете легко убедиться в этом, установив точку останова после await
и наблюдая за стеком вызовов. Вы увидите, что стек вызовов пропал после того, как первый результат await
. Или, что более точно, стек вызовов представляет собой код, который продолжает метод async
, а не код, который первоначально запустил метод async
.
На уровне реализации async
/ await
больше похоже на обратные вызовы, чем что-либо еще. Когда метод достигает await
, он помещает свою структуру конечного автомата в кучу (если это еще не сделано) и подключает обратный вызов. Этот обратный вызов запускается (вызывается напрямую), когда задача завершается, и это продолжает выполнение метода async
. Когда этот async
метод завершается, он выполняет свои задачи, и все, что await
в этих задачах, затем вызывается для продолжения выполнения. Итак, если вся последовательность задач завершена, вы фактически получите стек вызовов, который является инверсией стека причинности.
Я хотел бы сбросить все трассировки стека, даже те, у которых нет своего потока в настоящее время, так что я могу найти тупик.
Итак, здесь есть пара проблем. Во-первых, не существует глобального списка всех Task
объектов (или, в более общем смысле, объектов, подобных задачам). И это было бы трудно получить.
Во-вторых, для каждого асинхронного метода / задачи в любом случае нет причинно-следственной цепочки. Компилятор не генерирует его, потому что это не обязательно для правильной работы.
Нельзя сказать, что ни одна из этих проблем непреодолима - просто сложно. Я поработал над проблемой цепочки причинно-следственных связей с моей библиотекой AsyncDiagnostics . На данный момент он довольно старый, но его довольно легко обновить до. NET Core. Он использует PostSharp для изменения кода, созданного компилятором для каждого метода, и отслеживания цепочек причинно-следственной связи вручную. Получение списка всех типов задач и связывание причинно-следственных связей с каждым из них - еще одна проблема, которая, вероятно, потребует использования прикрепленного профилировщика. Мне известно о других компаниях, которые хотели это решение, но ни одна из них не уделила времени, необходимому для его создания; все они сочли более эффективными проводить проверку кода, аудит и обучение разработчиков.