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.