Не попадитесь на накрученные каналы! Узнайте, не накручивает ли канал просмотры или подписчиков Проверить канал на накрутку
Прикрепить Телеграм-аккаунт Прикрепить Телеграм-аккаунт

Телеграм канал «.NET Разработчик»

.NET Разработчик
3.2K
7.6K
29
2
17.0K
Дневник сертифицированного .NET разработчика. Заметки, советы, новости из мира .NET и C#.

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Подписчики
Всего
6 766
Сегодня
+1
Просмотров на пост
Всего
938
ER
Общий
13.86%
Суточный
10.02%
Динамика публикаций
Telemetr - сервис глубокой аналитики
телеграм-каналов
Получите подробную информацию о каждом канале
Отберите самые эффективные каналы для
рекламных размещений, по приросту подписчиков,
ER, количеству просмотров на пост и другим метрикам
Анализируйте рекламные посты
и креативы
Узнайте какие посты лучше сработали,
а какие хуже, даже если их давно удалили
Оценивайте эффективность тематики и контента
Узнайте, какую тематику лучше не рекламировать
на канале, а какая зайдет на ура
Попробовать бесплатно
Показано 7 из 3 203 постов
Смотреть все посты
Пост от 26.10.2026 08:02
106
0
1
День 2826. #ЗаметкиНаПолях #Async
Ещё Раз Про Async/Await в C#. Часть 6. Начало
В предыдущей части мы декомпилировали асинхронный метод и рассмотрели конечный автомат, который создаёт компилятор. В этой части мы напишем такой код. Создадим MyThreadPool и MyTask — с нуля. Это будет полноценная реализация, с которой компилятор C# сможет без проблем взаимодействовать через оператор await.
Для выполнения продолжений объекту Task требуется некая среда. Пул потоков — это, по сути, очередь и фиксированный набор потоков, которые извлекают из неё задачи:
// MyThreadPool.cs
public static class MyThreadPool
{
private static readonly
BlockingCollection<(Action Work, ExecutionContext? Context)>
WorkItems = [];

static MyThreadPool()
{
for (var i = 0; i < Environment.ProcessorCount; i++)
{
var thread = new Thread(WorkerLoop)
{
IsBackground = true
};
thread.Start();
}
}

public static void QueueUserWorkItem(Action work)
=> WorkItems.Add((work, ExecutionContext.Capture()));

private static void WorkerLoop()
{
foreach (var (work, context) in
WorkItems.GetConsumingEnumerable())
{
if (context is null)
work();
else
ExecutionContext.Run(context,
static state => ((Action)state!)(), work);
}
}
}
Стоит обратить внимание на контекст выполнения (ExecutionContext): поток, в котором выполняется задача, — это не тот поток, который поместил её в очередь, поэтому любой контекст, присутствовавший в исходном потоке, был бы утрачен. Захват контекста в момент постановки задачи в очередь и его восстановление в момент выполнения позволяет данным «пережить» такой перенос. Контекст выполнения мы рассмотрим подробнее далее в этой серии.
Установка IsBackground в true означает, что такие потоки не препятствуют завершению процесса. Так ведут себя потоки в настоящем пуле.
Состояние, результат, продолжение
Структуру объекта задачи мы рассматривали в 3й части, и MyTask содержит всё, что нужно:
// MyTask.cs
public sealed class MyTask
{
private readonly Lock _lock = new();

private bool _completed;
private Exception? _exception;
private Action? _continuation;
private ExecutionContext? _continuationContext;

public bool IsCompleted
{
get
{
lock (_lock)
{
return _completed;
}
}
}
…
Для простоты продолжение тут одно, а не список.
Завершение задачи – это лишь задание значений полей и выполнение ожидаемого действия:
// MyTask.cs
public void SetResult()
=> Complete(exception: null);
public void SetException(Exception exception)
=> Complete(exception);

private void Complete(Exception? exception)
{
Action? continuation;
ExecutionContext? context;

lock (_lock)
{
if (_completed)
throw new InvalidOperationException("Задача уже выполнена");

_completed = true;
_exception = exception;

continuation = _continuation;
context = _continuationContext;
}

// Планирование выполняется вне блокировки
if (continuation is not null)
Schedule(continuation, context);
}
Обратите внимание, зачем здесь предусмотрена блокировка. Рассмотрим возможный сценарий:
Поток A вызывает OnCompleted «ожидателя» для регистрации продолжения.
Поток B завершает задачу.
Если поток B успевает первым, то продолжения ещё нет, и поток A должен выполнить его самостоятельно. Если же первым оказывается поток A, то поток B обнаруживает наличие продолжения и выполняет его. Недопустима ситуация, при которой оба потока решают, что задачу выполнит другой, либо оба берутся за её выполнение. Поэтому при регистрации проверка флага завершённости задачи выполняется под той же блокировкой:
// MyTask.cs
private void AddContinuation(
Action continuation,
ExecutionContext? context)
{
var alreadyCompleted = false;

lock (_lock)
{
if (_completed)
alreadyCompleted = true;
else
{
_continuation = continuation;
_continuationContext = context;
}
}

// Регистрация продолжения на завершившейся задаче
// всё равно должна выполнить продолжение.
if (alreadyCompleted)
Schedule(continuation, context);
}
Предупреждение: никогда не вызывайте продолжение, удерживая блокировку. Продолжение — это произвольный пользовательский код: он может завершить другую задачу, захватить другую блокировку или снова обратиться к текущей. Выполнение такого кода внутри блока lock (_lock) превращает состояние «гонки данных» во «взаимную блокировку» — что ещё хуже, поскольку такая проблема никак себя не проявляет.
Планирование (Schedule) — этап, на котором захваченный контекст продолжения получает приоритет над контекстом потока, завершающего задачу:
// MyTask.cs
private static void Schedule(
Action continuation,
ExecutionContext? context)
{
if (context is null)
MyThreadPool.QueueUserWorkItem(continuation);
else
MyThreadPool.QueueUserWorkItem(
() => ExecutionContext.Run(context,
static state => ((Action)state!)(), continuation));
}
Обратите внимание, что продолжения всегда выполняются через пул потоков. Они никогда не запускаются непосредственно в том же потоке, который вызвал метод SetResult. Такой подход позволяет избежать целого ряда неожиданных ситуаций. В стандартном классе Task по умолчанию используется иной механизм — именно поэтому и существует опция RunContinuationsAsynchronously.
Блокировка
Иногда возникает необходимость в синхронном ожидании:
// MyTask.cs
public void Wait()
{
ManualResetEventSlim? completed = null;

lock (_lock)
{
if (!_completed)
{
completed = new ManualResetEventSlim();
_continuation = completed.Set;
_continuationContext = null;
}
}

completed?.Wait();

if (_exception is not null)
ExceptionDispatchInfo.Throw(_exception);
}
…
Это единственный метод, который блокирует поток. Здесь также происходит повторный выброс исключения — с помощью ExceptionDispatchInfo.Throw, что позволяет сохранить исходный стек трассировки при повторном выбрасывании исключения.
Делаем это «ожидаемым»
Всё необходимое для работы await уже присутствует. Паттерн awaitable требует наличия 4 элементов, и MyTask может предоставить их все, используя своё внутреннее состояние:
// MyTask.cs
public Awaiter GetAwaiter() => new(this);

public readonly struct Awaiter(MyTask task)
: INotifyCompletion
{
public bool IsCompleted => task.IsCompleted;

public void OnCompleted(Action continuation)
=> task.AddContinuation(continuation,
ExecutionContext.Capture());

public void GetResult() => task.Wait();
}
То, что GetResult вызывает Wait, настораживает, но это не блокирует выполнение: компилятор вызывает его только после того, как IsCompleted становится истинным или срабатывает продолжение, поэтому флаг уже установлен, и Wait сразу переходит к проверке исключения.
Awaiter — вложенный тип (поэтому он может обращаться к приватным членам MyTask) и структура только для чтения, поэтому ожидание не требует выделения памяти.

Окончание следует...

Источник: https://www.meziantou.net/writing-your-own-task-from-scratch.htm
Пост от 25.10.2026 08:01
90
0
0
День 2825. #ЧтоНовенького #VS
Сравнение Текущего Кода с Более Ранним Коммитом
Вы знаете, что код был изменён в одном из предыдущих коммитов, и хотите увидеть, как текущий код отличается от предыдущей версии. Вам поможет функция сравнения. Сравнение двух коммитов даёт частичный ответ, но не учитывает изменения, которые вы еще не закоммитили.
В Visual Studio 2026 (версия 18.11) в рабочий процесс работы с коммитами на уровне репозитория добавлена функция Compare with Working Tree (Сравнить с рабочим деревом). Теперь можно сравнить коммит из истории с текущим состоянием кода, не переключаясь на этот коммит и не покидая текущую ветку.
Откройте историю коммитов или воспользуйтесь списком веток, чтобы найти нужный коммит.
Щёлкните правой кнопкой мыши по коммиту и выберите пункт Compare with Working Tree.
В окне сравнения выберите измененные файлы, чтобы просмотреть различия (diff).
Контекстное меню коммита в Git-репозитории
Выбранный коммит служит отправной точкой для сравнения с вашим рабочим деревом. Visual Studio отобразит все отличающиеся файлы, не только те, что были изменены в выбранном коммите, но и любые другие различия.
Сравнение «бок о бок» файла Calculator.cs (в коммите из истории и в рабочем дереве); список измененных файлов отображается справа
Подробнее о том, как отследить момент появления проблемы или переключиться на конкретный коммит для запуска более старой версии кода, читайте в документации «Просмотр репозиториев Git и сравнение ветвей в Visual Studio».
Источник: https://devblogs.microsoft.com/visualstudio/compare-todays-code-with-an-earlier-commit/
Изображение
👍 1
Пост от 24.10.2026 08:03
286
0
4
День 2824. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.

51. Стратегии интеграционного тестирования
«Опишите ваш подход к интеграционному тестированию .NET-приложений. Расскажите, как вы планируете, реализуете и автоматизируете такие тесты, а также какие инструменты или фреймворки при этом используете».

Хороший ответ
Интеграционное тестирование в .NET-приложениях предполагает объединение модулей и их проверку как единого целого, чтобы убедиться, что взаимодействие между ними происходит ожидаемым образом. Мой подход к интеграционному тестированию включает следующие этапы:

1. Определение сценариев тестирования: выявление критических точек интеграции в приложении, где происходит взаимодействие между различными модулями, сервисами или внешними системами.

2. Подготовка тестовых данных: планирование и создание данных, необходимых для каждой точки интеграции, чтобы имитировать реальные сценарии использования.

3. Настройка тестовой среды: создание отдельной среды, максимально приближённой к проду, что позволяет избежать расхождений между результатами тестов и реальным поведением системы.

Для написания тестов можно использовать xUnit, а также пакет Microsoft.AspNetCore.TestHost, который предоставляет тестовый сервер для запуска приложения. Пример организации теста:
public class IntegrationTests : IDisposable
{
private readonly TestServer _server;
private readonly HttpClient _client;

public IntegrationTests()
{
var builder = WebApplication.CreateBuilder();
// … Добавляем сервисы …
builder.Services.AddControllers();
_server = new TestServer(
new WebHostBuilder().UseStartup<Startup>());
_client = _server.CreateClient();
}

[Fact]
public async Task TestEndpointReturnsSuccess()
{
var response = await
_client.GetAsync("/api/values");

response.EnsureSuccessStatusCode();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

public void Dispose()
{
_client.Dispose();
_server.Dispose();
}
}
Автоматизация интеграционного тестирования
- Непрерывная интеграция: выполнение интеграционных тестов запускаются в рамках CI-конвейера, чтобы гарантировать их запуск при каждом значимом изменении кодовой базы.
- Отчётность: подробное протоколирование и формирование отчётов о результатах в CI-конвейере позволит оперативно выявлять сбои и устранять их причины.

Лучшие практики
- Изоляция сценариев тестирования: несмотря на то что интеграционные тесты фокусируются на взаимодействии между компонентами, важно изолировать сценарии тестирования, привязывая их к конкретным точкам интеграции. Это упрощает поиск неисправностей и повышает наглядность тестов.
- Использование заглушек для внешних зависимостей, чтобы имитировать внешние системы (см. подробнее о заглушках). Это уменьшает количество зависимостей, а также повышает надёжность и скорость выполнения тестов.
- Мониторинг производительности: важно следить за тем, сколько времени занимают интеграционные тесты, чтобы они не стали «узким местом» в процессе разработки.

Такой подход гарантирует, что интеграционные тесты будут всесторонними и эффективными, а также внесут реальный вклад в общее качество приложения.

Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
Пост от 23.10.2026 08:00
216
0
0
День 2823. #СтатьяДня #AltenarCollab
Heartbeat: как мы создали систему управления системой вместо обычных сигналов
Обычно никто не обращает особого внимания на вспомогательные сервисы вроде мониторинга здоровья (HeartBeat). Они обслуживающие, клиент не видит ошибок в них, да и вообще не имеет к ним доступа. Поэтому (если только у команды не было негативного опыта и – больше того – извлечённых из этого опыта правильных уроков), заботятся об их правильной работе или отказоустойчивости вроде как в последнюю очередь. Есть? Хорошо. Что-то там мониторят? Отлично. А правильно мониторят? Могут своевременно сообщить о сбое до того, как вам начнут звонить разгневанные клиенты? Этот ваш HeartBeat, он вообще, вот прямо сейчас, работает?

А что, если система представляет собой высоконагруженный поток данных в реальном времени, неполадки в одном из сервисов обнаруживаются клиентами также в реальном времени, и устранить их желательно не завтра, после утреннего кофе, а также в реальном времени?

Александр Илларионов из Altenar в своей статье на Хабре рассказывает, как они разрабатывали такую систему, которая могла не просто мониторить real-time сервисы, но и своевременно поднимать тревогу и даже предпринимать некоторые действия, вроде отключения сбоящего сервиса.

Статье немного не хватает последовательности, и автор упускает из повествования некоторые детали. Очевидно, он «в контексте» и понимает, что, зачем и почему. Мне этого понимания в некоторых местах не хватало. Например:
Именно поэтому для нас принципиально важна последовательность в этих действиях: сначала заблокировать трафик, а потом уже отключить его и сообщить клиенту о проблеме.
При этом ранее не упоминалось, что значит заблокировать трафик и чем это отличается от отключения.

Но в принципе тема довольно интересная. Хотелось бы, чтобы Александр её развил и рассказал поподробнее. Возможно, в серии статей. Какие конкретно сервисы мониторинга и управления были созданы, как распределялись между ними обязанности и, возможно, детали их реализации.
👍 2
Пост от 22.10.2026 08:03
80
0
2
День 2822. #ЗаметкиНаПолях
Таймаут, Который Никогда не Срабатывал

У нас был такой код:
public async Task<Result<TData>> SendAsync<TData>(
Func<HttpRequestMessage> requestFactory,
RequestOptions? opts = null,
CancellationToken ct = default)
{
opts ??= new RequestOptions();
var policy = opts.Policy ?? DefaultPollyPolicy;
var result = await
policy.ExecuteAndCaptureAsync(async () =>
{
using var request = requestFactory();

if (ct == CancellationToken.None)
{
// Если таймаут не задан, ставим 5 сек
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
ct = cts.Token;
}

using var response =
await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct);
// …
});
// …
}
Отправка запроса выполняется с помощью HttpClient, который обёрнут в политику Polly (по умолчанию настроенную на одну повторную попытку). Если вызывающий код не передаёт токен отмены, мы создаём свой токен с тайм-аутом 5 секунд.
Выглядит логично. Сможете найти здесь ошибку? Напишите в комментариях, если нашли, не читая дальше.

На самом деле их тут целых две :)

Ошибка 1. Тайм-аут никогда не срабатывает
Конструкция using var освобождает ресурс по завершении области видимости, в которой она объявлена. В данном случае такой областью является блок if, а не лямбда-выражение. В результате CancellationTokenSource уничтожается (вызывается метод Dispose()) в момент закрывающей фигурной скобки — ещё до того, как запрос будет отправлен. При вызове Dispose() на CancellationTokenSource его внутренний таймер также уничтожается. Токен, полученный из этого объекта, формально продолжает «работать» (исключения не возникает), но он никогда не перейдёт в отменённое состояние.

Небольшой пример для воспроизведения:
var ct = CancellationToken.None;
var sw = Stopwatch.StartNew();
if (ct == CancellationToken.None)
{
using var cts =
new CancellationTokenSource(TimeSpan.FromSeconds(1));
ct = cts.Token;
}

try
{
await Task.Delay(TimeSpan.FromSeconds(3), ct);
Console.WriteLine($"Завершено спустя
{sw.Elapsed.TotalSeconds:F1}сек");
}
catch (Exception e)
{
Console.WriteLine($"{e.GetType().Name} спустя
{sw.Elapsed.TotalSeconds:F1}сек");
}

// Вывод
Завершено спустя 3.0сек
Тайм-аут составляет 1 секунду, однако задержка длится все 3 секунды.

Таким образом, в изначальном коде единственным действующим тайм-аутом остается HttpClient.Timeout, значение которого по умолчанию составляет 100 секунд.

Ошибка 2. При повторной попытке используется тот же токен
Токен отмены ct является параметром метода, и лямбда-выражение захватывает его. Следовательно, когда лямбда-выражение присваивает ему значение, меняется не локальная копия, а одна и та же переменная при каждом вызове лямбды.

При выполнении повторной попытки через Polly условие ct == CancellationToken.None перестаёт быть истинным. В результате блок if пропускается, и для повторной попытки используется токен из первой попытки — тот самый, который относится к уже уничтоженному объекту CancellationTokenSource.

Из этого также следует, что простого переноса конструкции using var за пределы блока if недостаточно. Это решило бы проблему для первой попытки, но повторная попытка все равно выполнялась бы без тайм-аута.

Исправление:
using var timeoutCts = ct == CancellationToken.None
// таймаут 5 сек
? new CancellationTokenSource(TimeSpan.FromSeconds(5))
: null;
var token = timeoutCts?.Token ?? ct;

using var response =
await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
Теперь CancellationTokenSource существует на протяжении всей попытки, а каждая попытка получает собственный локальный токен, что исключает перезапись захваченного параметра.

Источник: https://josef.codes/the-timeout-that-never-fired/
👎 1
Пост от 21.10.2026 08:01
58
0
4
День 2821. #ЗаметкиНаПолях #Async
Ещё Раз Про Async/Await в C#. Часть 5
Как компилятор C# переписывает асинхронный метод. Окончание
Начало
MoveNext — тело метода
MoveNext представляет собой исходное тело метода, переписанное так, чтобы его можно было вызывать многократно. Мы рассмотрим код более простого метода LoadAsync. Код метода SumPageSizesAsync существенно длиннее, но метод построен по тому же принципу. Посмотреть полный код можно здесь.
private void MoveNext()
{
int num = <>1__state;
string result;
try
{
TaskAwaiter awaiter;
if (num != 0)
{
awaiter = Task.Delay(1).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<>1__state = 0);
<>u__1 = awaiter;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
}
else
{
awaiter = <>u__1;
<>u__1 = default(TaskAwaiter);
num = (<>1__state = -1);
}
awaiter.GetResult();
result = page;
}
catch (Exception exception)
{
<>1__state = -2;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<>t__builder.SetResult(result);
}
Рассматривайте это как два прохода одного и того же кода.
Первый вызов. Значение <>1__state равно -1, поэтому условие num != 0 выполняется, и метод исполняет код до оператора await. Он получает объект ожидания и проверяет его свойство IsCompleted. Если операция уже завершена, блок if полностью пропускается: приостановки выполнения не происходит, обратный вызов не регистрируется, дополнительное выделение памяти не требуется — выполнение просто переходит к методу GetResult(). Это «быстрый путь»; благодаря ему асинхронные методы, в которых все операции await завершаются синхронно, практически не создают накладных расходов.
Если же операция ещё не завершена, состояние (state) устанавливается в 0 (чтобы при следующем вызове было понятно, с какого места продолжить), объект ожидания сохраняется в поле <>u__1 (чтобы он оставался доступным), а метод AwaitUnsafeOnCompleted даёт указание построителю снова вызвать MoveNext по завершении операции.
Второй вызов. Значение <>1__state равно 0, поэтому выполняется ветка else. Объект ожидания считывается из поля, само поле очищается (чтобы не удерживать ссылку), состояние возвращается к значению -1, а выполнение продолжается с вызова GetResult() — в той точке, где остановился первый вызов.
Оператор await — это, по сути, switch, работающий на основе значения поля, в сочетании с регистрацией обратного вызова. Каждый вызов MoveNext проходит по одному из этих путей:

Важно:
Блок try/catch добавляет компилятор. Так любое исключение, возникшее в теле метода, не выбрасывается наружу, а передается в SetException. Поэтому исключение в асинхронном методе никогда не проявляется непосредственно в месте вызова: оно перехватывается и помещается в возвращаемый объект Task, становясь доступным только в момент использования оператора await.
Блок finally из метода SumPageSizesAsync будет вложен в блок try, добавленный компилятором.
Примечание: AwaitUnsafeOnCompleted — это «небезопасный» вариант; который не выполняет повторный захват ExecutionContext. Это не ошибка: ExecutionContext уже был захвачен при вызове Start, поэтому повторный захват при каждом использовании await был бы излишней тратой ресурсов. «Безопасный» метод AwaitOnCompleted применяется для тех объектов ожидания, которые реализуют только интерфейс INotifyCompletion, а не ICriticalNotifyCompletion.
Структура в конфигурации Release, класс — в Debug
В конфигурации Release конечный автомат представляет собой структуру. Она размещается в стеке, и если метод ни разу не приостанавливается, то метод, завершающийся синхронно, вообще не выделяет памяти для своего конечного автомата. В тот момент, когда происходит приостановка, построитель переносит («упаковывает») конечный автомат в кучу, поскольку стек метода вскоре будет уничтожен.
В конфигурации Debug конечный автомат реализован как класс, который создаётся в куче при каждом вызове метода. Это работает медленнее, зато объект существует на протяжении всего времени выполнения метода — это необходимо отладчику, чтобы вы могли видеть значения локальных переменных при пошаговом выполнении кода.
Какова цена?
Абсолютные цифры не важны, важно понимать порядок. Асинхронность не бесплатная, даже если приостановки не происходит: в данном случае на создание объектов Task затрачивается пара сотен байт, а скорость выполнения не сильно будет отличаться от синхронного метода (в разы, но не на порядки). Приостановка выполнения — это совсем другое дело: затраты памяти превышают килобайт, а время выполнения увеличивается в сотни раз. Это объясняется тем, что каждая приостановка требует упаковки конечного автомата, регистрации продолжения и повторного планирования задачи в потоке из пула.
Это вовсе не аргументы против асинхронности. Асинхронность оправдана, когда ожидаемая операция действительно выполняется долго; в противном случае накладные расходы становятся вполне ощутимыми. Если метод ожидает результат, который уже находится в кэше, он проходит по «быстрому пути» с минимальными затратами. Если же метод приостанавливается на каждой итерации интенсивного цикла, его стоит переработать.
Источник: https://www.meziantou.net/how-the-csharp-compiler-rewrites-an-async-method.htm
Изображение
Пост от 20.10.2026 08:00
315
0
13
День 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
👍 3
Смотреть все посты