Start Debugging

Runtime Async из .NET 11 заменяет state machines более чистыми трассировками стека

Runtime Async в .NET 11 переносит обработку async/await из state machines, генерируемых компилятором, в саму среду выполнения, давая читаемые трассировки стека, корректные точки останова и меньше выделений в куче.

Если вы когда-либо смотрели на асинхронную трассировку стека в .NET, пытаясь выяснить, какой метод фактически выбросил исключение, вы знаете эту боль. Инфраструктура state machine, генерируемая компилятором, превращает простую цепочку из трёх вызовов методов в стену из AsyncMethodBuilderCore, MoveNext и искажённых обобщённых имён. .NET 11 Preview 2 поставляет preview-функцию Runtime Async, которая исправляет это на самом глубоком уровне: сама CLR теперь управляет асинхронной приостановкой и возобновлением вместо компилятора C#.

Как это работало раньше: state machines повсюду

В .NET 10 и ранее пометка метода как async указывает компилятору C# переписать его в struct или класс, реализующий IAsyncStateMachine. Каждая локальная переменная становится полем в этом сгенерированном типе, и каждый await — это переход состояния внутри MoveNext(). Результат корректен, но имеет свои издержки:

async Task<string> FetchDataAsync(HttpClient client, string url)
{
    var response = await client.GetAsync(url);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadAsStringAsync();
}

Когда исключение возникает внутри FetchDataAsync, трассировка стека включает кадры для AsyncMethodBuilderCore.Start, сгенерированного <FetchDataAsync>d__0.MoveNext() и обобщённой сантехники TaskAwaiter. Для цепочки из трёх async-вызовов вы легко увидите 15+ кадров, где лишь три несут осмысленную информацию.

Что меняет Runtime Async

С включённым Runtime Async компилятор больше не эмитирует полную state machine. Вместо этого он помечает метод метаданными, которые сообщают CLR обрабатывать приостановку нативно. Среда выполнения держит локальные переменные на стеке и сбрасывает их в кучу только тогда, когда выполнение фактически пересекает границу await, которая не может завершиться синхронно. Практический результат: меньше выделений и драматически более короткие трассировки стека.

Async-цепочка из трёх методов вроде OuterAsync -> MiddleAsync -> InnerAsync производит трассировку стека, которая отображается напрямую на ваш исходный код:

at Program.InnerAsync() in Program.cs:line 24
at Program.MiddleAsync() in Program.cs:line 14
at Program.OuterAsync() in Program.cs:line 8

Без синтетического MoveNext, без AsyncMethodBuilderCore, без обобщений с искажёнными типами. Только методы и номера строк.

Отладка теперь действительно работает

Preview 2 добавила критическое исправление: точки останова теперь корректно привязываются внутри методов runtime-async. В Preview 1 отладчик иногда пропускал точки останова или приземлялся на неожиданных строках при пошаговом прохождении границ await. С Preview 2 вы можете установить точку останова на строке после await, попасть в неё и инспектировать локальные переменные нормально. Шаг через await приземляется на следующую инструкцию, а не внутри инфраструктуры среды выполнения.

Это также приносит пользу инструментам профилирования и диагностическому логированию. Всё, что вызывает new StackTrace() или читает Environment.StackTrace во время выполнения, теперь видит реальную цепочку вызовов, что делает структурированное логирование и пользовательские обработчики исключений более полезными без дополнительной фильтрации.

Включение Runtime Async

Это всё ещё preview-функция. Подключитесь, добавив два свойства в свой .csproj:

<PropertyGroup>
  <Features>runtime-async=on</Features>
  <EnablePreviewFeatures>true</EnablePreviewFeatures>
</PropertyGroup>

Поддержка со стороны CLR включена по умолчанию в .NET 11, поэтому вам больше не нужно устанавливать переменную окружения DOTNET_RuntimeAsync. Флаг компилятора — единственный переключатель.

За чем следить

Runtime Async ещё не является дефолтным для продакшн-кода. Команда .NET всё ещё работает над крайними случаями с tail calls, определёнными ограничениями generics и взаимодействием с существующими диагностическими инструментами. Если вы уже на превью .NET 11 и хотите попробовать в тестовом проекте, две строки MSBuild выше — это всё, что вам нужно.

Полные детали Runtime Async в заметках о выпуске .NET 11 Preview 2 и на странице What’s new in .NET 11 runtime на Microsoft Learn.

< Назад