async2 — Завершение Эксперимента с Асинхронностью в .NET. Окончание
Начало
async — функция компилятора
Текущая реализация async и await — это функция компилятора. Компилятор генерирует конечный автомат для async -метода. Среда выполнения ничего не знает об async и await. Нет никаких следов ключевого слова async в IL или в JIT-скомпилированном коде. И вот с этого начался эксперимент, подробно описанный здесь.
async — функция среды выполнения
Целью эксперимента было переместить async и await в среду выполнения. Это позволило бы среде выполнения иметь больше контроля над самим шаблоном. При этом также была бы другая семантика.
async2 не сохраняет и не восстанавливает контекст синхронизации и контекст исполнения на границах функций, вместо этого позволяя вызывающим методам наблюдать изменения. С ExecutionContext это изменяет поведение AsyncLocal.
Сегодня AsyncLocal используется для хранения данных, которые передаются с логическим контекстом вызова. Они копируются в новый контекст. Тем не менее, если функция глубоко в стеке вызовов изменяет значение AsyncLocal, вызывающий метод не увидит обновлённое значение, его увидят только методы ниже по логическому асинхронному потоку. Например:
await new AsyncLocalTest().Outer();
public class AsyncLocalTest
{
private readonly AsyncLocal<string>
_al = new();
public async Task Outer()
{
_al.Value = "Out";
Console.WriteLine($"Outer: {_al.Value}");
await Inner();
Console.WriteLine($"Outer: {_al.Value}");
}
private async Task Inner()
{
_al.Value = "In";
Console.WriteLine($"Inner: {_al.Value}");
await Task.Yield();
Console.WriteLine($"Inner: {_al.Value}");
}
}
Вывод:
Outer: Out
Inner: In
Inner: In
Outer: Out
В async2 изменения не «откатываются», что приводит к другому выводу:
Outer: Out
Inner: In
Inner: In
Outer: In
Сравнение с текущей реализацией
Команда обнаружила, что подход с помещением асинхронности в JIT может дать наилучшие результаты в целом. Вот основные отличия:
1. Производительность
Async обычно медленнее, чем async2, особенно для глубоких стеков вызовов, где async2 имеет производительность, сопоставимую с синхронным кодом в сценариях без приостановки исполнения.
2. Обработка исключений
В async медленно и неэффективно, вызывает паузы GC и влияет на отзывчивость приложений. В async2 улучшена.
3. Ограничение глубины стека
Присутствует в async, что может вызвать проблемы для глубоких стеков вызовов. Нет явных ограничений в async2.
4. Потребление памяти
В async обычно ниже, особенно в сценариях с большим количеством приостановленных задач. В async2 более высокое потребление памяти из-за захвата целых стековых кадров и регистров, но все ещё приемлемо по сравнению с другими факторами, такими как время паузы.
Полный документ здесь.
Что дальше?
Пока это всего лишь эксперимент, который может привести к замене async через несколько лет. Да, может пройти некоторое время, прежде чем это будет выпущено. А для переходной фазы должен быть interop для async <-> async2. В любом случае - очень хорошая отправная точка. Ждём async2, как юнионов.
Источник: https://steven-giesel.com/blogPost/59752c38-9c99-4641-9853-9cfa97bb2d29/async2-the-net-runtime-async-experiment-concludes