Русский
Открыть в Telegram

Пост День 2819. #ЗаметкиНаПолях #Async

19 октября 2026
.
.NET Разработчик
День 2819. #ЗаметкиНаПолях #Async Ещё Раз Про Async/Await в C#. Часть 4 Паттерн awaitable: создание объектов, поддерживающих ожидание Если спросить большинство C#-разработчиков, что можно использовать с оператором await, они ответят: «Task». Это распространённый, но далеко не единственный случай. Механизм await работает так же, как и foreach, на основе структуры. Компилятор ищет определенную «форму» (набор элементов), и Task как раз ей соответствует. Понимание этой структуры поможет разобраться в конечном автомате, который генерируется компилятором (о нём позже). Кроме того, это позволит создавать собственные «ожидаемые» объекты. Структура Чтобы выражение await expr успешно скомпилировалось, у объекта expr должен быть метод GetAwaiter(). Возвращаемый им тип — так называемый «awaiter» (объект ожидания) — должен содержать три элемента и реализовывать один интерфейс: public readonly struct MyAwaiter : INotifyCompletion { // завершилась ли уже операция? public bool IsCompleted { get; } // значение, либо повторный выброс исключения public TResult GetResult(); // вызывается при завершении public void OnCompleted(Action continuation); } Вот и весь контракт. Компилятор выполняет привязку этих членов по их именам, что имеет два важных следствия: 1. Метод GetAwaiter может быть методом расширения. Это позволяет сделать тип «ожидаемым», даже если вы не являетесь его владельцем: public static class TimeSpanAwaitableExtensions { public static TaskAwaiter GetAwaiter(this TimeSpan timeSpan) => Task.Delay(timeSpan).GetAwaiter(); } … await TimeSpan.FromMilliseconds(200); 2. Объект ожидания может быть структурой. TaskAwaiter, ValueTaskAwaiter и YieldAwaitable.YieldAwaiter являются структурами; поэтому при ожидании уже завершённой задачи не происходит выделения памяти под объект ожидания. Как используются эти три члена Для каждого вызова await компилятор генерирует примерно следующий код: var awaiter = expr.GetAwaiter(); if (!awaiter.IsCompleted) { // сохранение состояния // регистрация продолжения // возврат к вызывающему методу awaiter.OnCompleted(MoveNext); return; } var result = awaiter.GetResult(); - Свойство IsCompleted определяет, будет ли выполнение метода приостановлено. Если оно возвращает true, выполнение продолжается без прерываний, в том же потоке, без планирования задачи и выделения дополнительной памяти. Это «быстрый путь», ради которого и существует ValueTask. - Метод OnCompleted вызывается только в том случае, если IsCompleted вернуло false. Он получает продолжение и отвечает за его последующий запуск. Где именно будет выполнено это продолжение — решать вам. - Метод GetResult вызывается в обоих случаях. Он возвращает результат операции и, что важно, именно здесь повторно выбрасываются исключения. Если операция завершилась неудачей, ожидающий объект сообщает об этом, выбрасывая исключение из GetResult, а не через какой-то отдельный канал для передачи ошибок. То, что GetResult может возвращать void, нормально: именно так ведёт себя await при работе с непараметризованным Task. ICriticalNotifyCompletion Существует еще один интерфейс — ICriticalNotifyCompletion, который добавляет метод UnsafeOnCompleted. Практически все реальные реализации ожидающих объектов поддерживают его. Разница заключается в работе с ExecutionContext. Ожидается, что OnCompleted захватит текущий ExecutionContext и восстановит его перед выполнением продолжения. UnsafeOnCompleted этого делать не обязан. Слово «Unsafe» звучит тревожно, но на деле всё не так страшно: создаваемый компилятором конечный автомат уже захватывает ExecutionContext при запуске метода и восстанавливает его при каждом вызове MoveNext. Повторный захват контекста для каждого await был бы излишней работой. Поэтому конечный автомат вызывает UnsafeOnCompleted, если ожидающий объект его предоставляет, а в противном случае использует «безопасную» версию (OnCompleted). Реализуйте оба интерфейса. В UnsafeOnCompleted должна выполняться основная логика, а OnCompleted может просто вызывать UnsafeOnCompleted — при условии, что у вас нет собственного контекстного состояния, которое нужно передавать дальше. Полезный объект, поддерживающий ожидание Task.Yield() возвращает управление в текущий SynchronizationContext (например, в UI-приложении это означает возврат в UI-поток). Иногда же требуется обратное: безусловно покинуть текущий поток и перейти в поток из пула потоков. public readonly struct ThreadPoolAwaitable : ICriticalNotifyCompletion { public ThreadPoolAwaitable GetAwaiter() => this; public bool IsCompleted => false; public void GetResult() { } public void OnCompleted(Action continuation) => UnsafeOnCompleted(continuation); public void UnsafeOnCompleted(Action continuation) => ThreadPool.UnsafeQueueUserWorkItem( static state => ((Action)state!)(), continuation); } … await default(ThreadPoolAwaitable); // дальнейший код выполняется в потоке из пула Особенности: - Тип сам является своим собственным «ожидателем». GetAwaiter() возвращает this. Это распространённый прием, который позволяет избежать лишнего выделения памяти и дополнительного уровня косвенности; YieldAwaitable использует такой же подход. - Свойство IsCompleted всегда возвращает false. Это сделано намеренно: если бы оно возвращало true, продолжение выполнялось бы синхронно в текущем потоке, а именно этого мы и стараемся избежать. - Это readonly-структура. Для операции ожидания не требуется выделение памяти в куче. Примечание: Тот же механизм, только наоборот, позволяет в UI-фреймворках использовать конструкцию await someControl.Dispatcher; для возврата в UI-поток. Метод OnCompleted объекта ожидания в данном случае помещает продолжение в диспетчер, а не в пул потоков. Когда стоит создавать свой объект ожидания Довольно редко. В большинстве случаев правильным выбором будут Task, ValueTask или TaskCompletionSource. Создание собственного объекта ожидания оправдано в следующих ситуациях: 1. Вы хотите, чтобы await означал «смену контекста» (как в случае с Task.Yield() или ThreadPoolAwaitable). При этом не нужно передавать какой-либо результат — важен только факт планирования выполнения. 2. Вы работаете с механизмом завершения операций, который существенно отличается от стандартного, и хотите избежать выделения памяти под объект Task для каждой операции. Для этого существует интерфейс IValueTaskSource (про него позже в этой серии). 3. Вы хотите сделать существующий тип «ожидаемым», не меняя его исходный код — здесь поможет прием с методами расширения. Важно помнить: собственный объект ожидания не поддерживает композицию. Он не будет работать с Task.WhenAll, WaitAsync или любыми другими механизмами, ожидающими объект Task. Если вам нужны такие возможности, просто возвращайте Task. Источник: https://www.meziantou.net/the-awaitable-pattern-writing-your-own-awaitable.htm
👍6
23 · 572 ·

Рядом в ленте

..NET РазработчикДень 2817. #ЧтоНовенького #VS Прекращаем Сборку После Первой Ошибки Мэдс Кристенсен продолжает поставлять небольшие и на первый взгляд очевидные улучшения проце..NET РазработчикДень 2818. #ВопросыНаСобеседовании Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.
это сообщение
..NET РазработчикДень 2820. #ЗаметкиНаПолях #Async Ещё Раз Про Async/Await в C#. Часть 5 Как компилятор C# переписывает асинхронный метод. Начало Вплоть до версии .NET 10, async
..NET Разработчик.NET Разработчик@NetDeveloperDiary · канал · Технологии
6 772подписчиков1 179средний охват поста
Лента площадки Открыть в Telegram

Открытая публичная лента из поискового индекса ChatCrawler — «Google по публичному Telegram»; обновляется по мере обхода площадки. Время — UTC.

Только публичный контент, официальный API Telegram. О проекте · Вопросы · Границы индекса · Убрать страницу из выдачи · Каталог · Поиск · Как мы считаем