.NET Разработчик
День 2820. #ЗаметкиНаПолях #Async
Ещё Раз Про Async/Await в C#. Часть 5
Как компилятор C# переписывает асинхронный метод. Начало
Вплоть до версии .NET 10, async и await не являются встроенными возможностями среды выполнения. Компилятор переписывает код метода с модификатором async. Он извлекает тело метода, переносит его в автоматически созданный тип, а исходный метод заменяет небольшим фрагментом кода, который инициализирует этот тип и запускает его метод. Всё остальное — планирование выполнения, обработка исключений, формирование значения, которое в итоге попадает в возвращаемый объект Task, — представляет собой обычный код, который вы могли бы написать самостоятельно. Сегодня рассмотрим небольшой асинхронный метод и проанализируем, во что его превращает компилятор.
Метод содержит все элементы, с которыми приходится работать компилятору: параметр, локальные переменные, значения которых должны сохраняться после await, два await разных типов, цикл, а также блок try/finally:
public static async Task<int>
SumPageSizesAsync(string[] pages)
{
var total = 0;
var sw = Stopwatch.StartNew();
try
{
foreach (var page in pages)
{
var content = await LoadAsync(page);
total += content.Length;
}
await Task.Yield();
return total;
}
finally
{
Console.WriteLine($"Loaded {pages.Length} pages in {sw.ElapsedMilliseconds}ms");
}
}
private static async Task<string>
LoadAsync(string page)
{
await Task.Delay(1);
return page;
}
Чтобы увидеть, что с методом делает компилятор, можно либо декомпилировать его с помощью ILSpy, либо использовать онлайн инструмент sharplab.io.
Что осталось от исходного метода
[AsyncStateMachine(typeof(<SumPageSizesAsync>d__0))]
public static Task<int> SumPageSizesAsync(string[] pages)
{
<SumPageSizesAsync>d__0 stateMachine = default(<SumPageSizesAsync>d__0);
stateMachine.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
stateMachine.pages = pages;
stateMachine.<>1__state = -1;
stateMachine.<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
По порядку:
1. Создаётся экземпляр конечного автомата.
2. Для него создаётся построитель — AsyncTaskMethodBuilder<int>. Он управляет объектом Task, который будут ожидать вызывающие стороны; именно он отвечает за завершение.
3. Параметры копируются в конечный автомат. Теперь они становятся его полями, поскольку код, который их использует, находится в другом месте.
4. Состояние устанавливается в значение -1 (не запущено, не приостановлено).
5. Вызывается метод Start, который запускает выполнение тела метода, после чего возвращается объект Task.
Сгенерированные имена с угловыми скобками не являются допустимыми идентификаторами в языке C#. Это сделано намеренно: они не могут конфликтовать с именами, которые вы задаёте в своём коде, и вы не сможете случайно сослаться на них.
Примечание: Атрибут [AsyncStateMachine] используют отладчики, профилировщики и анализаторы, чтобы связать метод с типом, который фактически содержит его логику.
Важное следствие: вызов асинхронного метода приводит к выполнению его тела до первого ожидания (await) операции, которая ещё не завершилась. Только после этого управление возвращается вызывающей стороне.
Сгенерированный тип
Ниже представлены поля конечного автомата:
private struct <SumPageSizesAsync>d__0 : IAsyncStateMachine
{
public int <>1__state;
public AsyncTaskMethodBuilder<int> <>t__builder;
public string[] pages;
private int <total>5__2;
private Stopwatch <stopwatch>5__3;
private string[] <>7__wrap3;
private int <>7__wrap4;
private TaskAwaiter<string> <>u__1;
private YieldAwaitable.YieldAwaiter <>u__2;
}
- <>1__state- указывает точку возобновления выполнения: -1 — выполняется, -2 — завершено; значения 0 и выше соответствуют отдельным операторам await, на которых выполнение может быть приостановлено.
- <>t__builder- объект-построитель, который управляет возвращаемым объектом Task.
- pages - параметр метода; сохраняет своё исходное имя.
- <total>5__2, <sw>5__3 - локальные переменные, которые требуются после выполнения оператора await; отчасти сохраняют свои исходные имена.
- <>7__wrap3, <>7__wrap4 - состояние цикла foreach: массив и текущий индекс. Компилятору они необходимы, чтобы сохранить контекст при выполнении await внутри цикла.
- <>u__1, <>u__2 - по одному слоту на каждый тип объектов ожидания; удерживают их, пока метод находится в приостановленном состоянии.
Важно:
1. Локальная переменная становится полем класса только в том случае, если она необходима после оператора await. Если же переменная используется исключительно в промежутке между двумя операторами await, она остаётся обычной локальной переменной. Поэтому асинхронный метод, сохраняющий большое количество данных, оказывается более «дорогим», чем метод, который этого не делает. Кроме того, перенос части логики в отдельный, синхронный метод позволяет уменьшить размер конечного автомата.
2. Слоты для объектов ожидания привязаны к их типу, а не к конкретному оператору await. Метод LoadAsync возвращает Task<string>, поэтому его «ожидатель» типа TaskAwaiter<string>. А Task.Yield() возвращает YieldAwaitable и его «ожидатель» другого типа. Сколько типов, столько и полей.
Окончание следует…
Источник: https://www.meziantou.net/how-the-csharp-compiler-rewrites-an-async-method.htm
19 · 611 ·