Пропустити навігацію EPAM

Async Runtime у .NET 11: огляд ключових оновлень

Лайфхаки
  • .NET

.NET 11 приносить істотні архітектурні зміни в роботу асинхронних методів завдяки впровадженню Runtime Async (також відомого як Managed Async Resume). Хоча ідея безпосереднього перенесення відповідальності за асинхронне виконання з компілятора на середовище виконання (runtime) почала активно обговорюватися ще з виходом .NET 9 у 2022 році, саме у .NET 11 ця технологія отримує значне покращення та стабілізацію. Наразі платформа перебуває на стадії прев’ю, а фінальний реліз очікується у листопаді 2026 року.

Ідея полягає в тому, що відповідальність за асинхронне виконання переноситься з компілятора безпосередньо на середовище виконання (runtime), що відкриває нові можливості для оптимізації асинхронного коду.

У цій статті ми детально розглянемо, що таке Runtime Async, проаналізуємо архітектуру асинхронного виконання та покажемо практичні переваги для розробників. Зокрема, ви дізнаєтеся про зменшення алокацій пам’яті, покращену відладку завдяки чистішим стек-трейсам (stack traces) та особливості впровадження цієї технології в CoreCLR.

Що таке Runtime Async у .NET 11

Еволюція асинхронного програмування в C#

Асинхронне програмування в C# пройшло довгий шлях — від появи класу Task у .NET 4.0 до введення ключових слів async та await у C# 5.0 у 2012 році. Тоді компілятор отримав здатність перетворювати асинхронний код у машину станів (state machine), приховуючи від розробника складність управління станами та контекстами. Ця модель стала стандартом для обробки операцій вводу-виводу та тривалих обчислень без блокування головного потоку.

Традиційна модель async/await через компілятор

З моменту появи async/await компілятор C# повністю відповідає за трансформацію асинхронних методів у структури state machine. Коли ви пишете async-метод, компілятор генерує структуру, що реалізує інтерфейс IAsyncStateMachine і відстежує прогрес методу через точки призупинення. Кожен асинхронний метод створює state machine зі спеціальними полями для локальних змінних, які живуть через await. Якщо метод не завершується синхронно, ця структура розміщується в купі (heap).

Такий підхід призводить до створення кількох фреймів state machine для кожного асинхронного методу. Згенерований код містить синтетичні методи MoveNext замість оригінальних назв, що ускладнює відладку. Крім того, runtime не розуміє асинхронну семантику і бачить лише звичайні виклики методів, що унеможливлює оптимізацію ланцюжків асинхронних викликів.

Новий підхід: асинхронне виконання в .NET 11 через runtime

У .NET 11 Runtime Async переносить розуміння асинхронних методів з компілятора безпосередньо в runtime. Замість генерації складних класів state machine компілятор створює простіший IL-код, анотований атрибутом MethodImpl(MethodImplOptions.Async). Сам runtime бере на себе відповідальність за призупинення та відновлення методів, управління збереженням стану та оптимізацію викликів між асинхронними методами.

Бібліотеки runtime .NET компілюються з увімкненим параметром runtime-async=on і більше не містять повного набору згенерованої компілятором інфраструктури state machine. Замість цього частина логіки призупинення та відновлення асинхронних операцій тепер виконується безпосередньо середовищем виконання (runtime). Це повністю покладається на асинхронність через runtime і дає змогу перенести весь додаток на нову модель асинхронного виконання в .NET 11, забезпечуючи ширше тестування та оптимізацію цієї функціональності. 

ПРИЄДНУЙСЯ ДО НАШОЇ КОМАНДИ

Архітектура асинхронного виконання Runtime Async

Як працює state machine у традиційному async/await

State machine, згенерована компілятором для async-методів, містить важливі поля для керування станом виконання. Поле <>1__state визначає поточний стан машини: значення -2 означає завершення операції, -1 — перший виклик MoveNext або активне виконання в іншому потоці, а значення 0 або більше сигналізують про призупинення методу на конкретному await. Поле <>t__builder — це AsyncTaskMethodBuilder, який відповідає за життєвий цикл async-методу, включно зі створенням Task, завершенням операції та обробкою призупинень.

Метод MoveNext() — центральний механізм state machine, який не повертає значень, а змінює стан за посиланням. Він перевіряє, чи завершився awaiter, і змінює стан машини відповідно. Якщо операція не може виконатися синхронно, ThreadPool отримує повідомлення про завершення асинхронної операції і викликає MoveNext() повторно для встановлення результату.

Нативна обробка async-методів через CLR

Runtime Async у .NET 11 змінює парадигму, дозволяючи безпосередньо керувати призупиненням і відновленням методів у CLR. Замість генерації класів state machine компілятор створює методи з атрибутом MethodImplOptions.Async, що дозволяє runtime розпізнавати їх як runtime-async. Runtime відстежує асинхронне виконання, керує збереженням стану методів і забезпечує оптимізації, які були недоступні у традиційному підході через компілятор.

Порівняння стек-трейсів: 13 фреймів проти 5

Найпомітніша різниця — у живих стек-трейсах, які бачать профайлери, відладчики та new StackTrace() під час виконання. Без runtime-async кожен асинхронний метод створює 13 фреймів, включно з повторюваними викликами AsyncMethodBuilderCore.Start. З увімкненим runtime-async стек-трейс скорочується до 5 фреймів, показуючи реальний ланцюжок викликів без синтетичної інфраструктури. Точки зупинки (breakpoints) тепер коректно прив’язуються всередині runtime-async методів, а відладчик може крокувати через await без переходу у згенерований компілятором код.

Налаштування Runtime Async через файл проєкту

Проєкти на net11.0 більше не потребують <EnablePreviewFeatures>true</EnablePreviewFeatures> для використання Runtime Async. Достатньо додати <Features>runtime-async=on</Features> у PropertyGroup файлі проєкту. Для відмови від асинхронного runtime на рівні проєкту можна встановити <UseRuntimeAsync>false</UseRuntimeAsync>.

Нові можливості Runtime Async для розробників

Оптимізації асинхронного коду без ручного втручання

Розробники отримують автоматичні оптимізації асинхронного коду в .NET 11 без необхідності переписувати наявні методи. Runtime розпізнає async-to-async виклики і оптимізує передачу управління між методами, що раніше вимагало повного циклу призупинення та відновлення для кожного виклику. CoreCLR відстежує асинхронне виконання на рівні runtime, що дозволяє застосовувати оптимізації, недоступні у традиційному підході.

Зменшення алокацій пам’яті та навантаження на heap

Локальні змінні залишаються на стеку за замовчуванням, на відміну від старої моделі, де кожна локальна змінна, що існує через await, переміщувалася у поле на heap. Runtime переносить змінні на heap лише якщо вони потребують збереження через призупинення. Якщо метод завершується без призупинення, алокації на heap не відбувається. Ранні тести спільноти показують зменшення алокацій на 25–33% для активних асинхронних шляхів. Водночас runtime агресивніше повторно використовує об’єкти продовження, знижуючи навантаження на виділення пам’яті в асинхронно-інтенсивному коді.

Покращена відладка через чистіші стек-трейси

Профайлери та відладчики бачать реальні відносини викликів методів завдяки нативному відстеженню асинхронного виконання runtime. Breakpoints коректно прив’язуються всередині runtime-async методів, а вікно стеку викликів відладчика показує фактичні методи без переходу у згенерований код. Ведення журналу діагностики отримує точнішу інформацію про стек під час виконання.

Зверніть увагу: інструменти діагностики можуть потребувати оновлення - без актуальних версій APM і профайлерів виконання runtime‑async може відображатися некоректно, спотворюючи стек‑трейси та метрики. Перед увімкненням runtime‑async оновіть інструменти та прогоніть перевірки, щоб переконатися в коректності діагностики та уникнути хибних інтерпретацій результатів.

Зменшення обсягу IL-коду та прискорення JIT-компіляції

Компілятор генерує простіший IL-код, анотований MethodImpl(MethodImplOptions.Async), замість складних структур state machine. Це зменшує обсяг IL-коду і прискорює JIT-компіляцію. Runtime Async підтримує компіляцію NativeAOT та ReadyToRun, розширюючи функціонал за межі JIT-коду до заздалегідь скомпільованих сценаріїв.

Сумісність з ValueTask та ConfigureAwait

ConfigureAwait(false) залишається дійсним, оскільки контролює захоплення контексту синхронізації, що відокремлено від алокації state machine. ValueTask зберігає своє місце для методів, які часто завершуються синхронно без операцій вводу-виводу. Проте існуючий код не стає автоматично ефективнішим при оновленні до .NET 11, оскільки оптимізатор працює лише для методів, скомпільованих з увімкненим параметром runtime-async=on. Щоб отримати переваги від цієї функції, потрібно перекомпілювати код під новий таргет із увімкненою підтримкою Runtime Async. 

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

Підтримка в CoreCLR та Native AOT

CoreCLR підтримка Runtime Async увімкнена за замовчуванням у Preview 1, що усуває потребу встановлення змінних середовища. Native AOT отримала фундаментальну підтримку, що дозволяє компілювати та діагностувати runtime-async методи, включно з підтримкою продовжень та інструментами компіляції. Runtime Async підтримує компіляцію NativeAOT та ReadyToRun, розширюючи функціонал за межі JIT-коду до заздалегідь скомпільованих сценаріїв. Додатки Native AOT публікуються для конкретних середовищ виконання, таких як Linux x64 або Windows x64.

Обмеження Preview 1 для runtime-бібліотек

Жодна з основних runtime-бібліотек не скомпільована з увімкненою підтримкою runtime-async у Preview 1. Очікується, що ця ситуація зміниться у наступних прев’ю-версіях. Зокрема, компіляція бібліотек System.* та ASP.NET Core з підтримкою runtime-async відкриє можливості для оптимізації продуктивності на рівні всієї платформи. Самі бібліотеки runtime .NET компілюються з runtime-async=on у фінальних версіях.

Дорожня карта до фінального релізу в листопаді 2026

.NET 11 запланований як реліз з підтримкою Standard Term Support; фінальна версія очікується у листопаді 2026 року. Preview 3 усунув вимогу прапорця preview features для проєктів net11.0 та додав підтримку ReadyToRun. Команда продукту запрошує зворотний зв’язок щодо змін продуктивності та розміру бібліотек.

Висновок

Runtime Async у .NET 11 — це істотна зміна архітектури асинхронного програмування. Переміщення відповідальності за асинхронне виконання безпосередньо в runtime дозволяє досягти значних оптимізацій: зменшення алокацій пам’яті на 25–33%, скорочення стек-трейсів з 13 до 5 фреймів, покращену відладку, простіший IL-код та підтримку Native AOT. З фінальним релізом у листопаді 2026 року .NET 11 відкриває нові можливості для створення високопродуктивних асинхронних систем.

Підписатися на новини

Чудово! Ми вже готуємо добірку актуальних новин для вас :)

Вибачте, щось пішло не так. Будь ласка, спробуйте ще раз.

* Обов'язкові поля

*Будь ласка, заповніть обов’язкові поля