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

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

15 октября 2026
.
.NET Разработчик
День 2815. #ЗаметкиНаПолях #Async Ещё Раз Про Async/Await в C#. Часть 3 Жизненный Цикл Task и TaskCompletionSource В предыдущих статьях этой серии мы рассматривали потоки и операционную систему. Эта статья посвящена объекту, занимающему промежуточное положение: Задача (Task). Он появился ещё до внедрения механизма async/await, и понимание его устройства упростит восприятие материала о преобразованиях, выполняемых компилятором, о которых будет позже. Задача — это своего рода «обещание» получить результат в будущем. Это не поток, не операция и не единица работы. Task хранит три элемента: состояние, результат (или исключение) и список действий, которые необходимо выполнить по завершении задачи. Жизненный цикл Задача может быть запущена одним из двух способов: либо создаётся уже завершенной, либо создаётся в состоянии ожидания и завершается позже. В дальнейшем она переходит из одного состояния в другое, двигаясь только в одном направлении: Три конечных состояния важны, поскольку при использовании await они ведут себя по-разному: • RanToCompletion (успешное завершение) — возвращает полученное значение, • Faulted (завершена с ошибкой) — повторно выбрасывает исключение, • Canceled (отменена) — выбрасывает исключение OperationCanceledException, что отличается от завершения с ошибкой. Код, который воспринимает отмену как ошибку, обычно неверен. Задача никогда не выходит из конечного состояния, и завершить её можно только один раз. Именно поэтому API содержит методы SetResult и TrySetResult: первый выбрасывает исключение, если задача уже завершена, а второй возвращает false. Если существует вероятность того, что за завершение одной и той же задачи могут одновременно бороться два процесса, используйте методы Try<…>. TaskCompletionSource: ожидаем что угодно! Большинство задач создаются на основе операций, которые возвращают Task. TaskCompletionSource<T> позволяет создать задачу из того, что её изначально не предоставляет: например, из события, обратного вызова или сообщения, поступившего в очередь. В примере ниже событие «следующее срабатывание FileSystemWatcher» преобразуется в объект, поддерживающий await: public static Task<string> WaitForNextChangeAsync( this FileSystemWatcher watcher, CancellationToken ct = default) { var tCS = new TaskCompletionSource<string>( TaskCreationOptions.RunContinuationsAsynchronously); FileSystemEventHandler? handler = null; CancellationTokenRegistration registration = default; handler = (_, e) => { watcher.Changed -= handler; registration.Dispose(); tCS.TrySetResult(e.FullPath); }; watcher.Changed += handler; registration = ct.Register(() => { watcher.Changed -= handler; tCS.TrySetCanceled(ct); }); return tCS.Task; } Три важных момента: • TrySetResult, а не SetResult. Возникает состояние гонки между событием и обратным вызовом отмены; тот, кто проигрывает в этой гонке, не должен выбрасывать исключение. • Отмена подписки. В противном случае обработчик будет удерживать в памяти замыкание, TaskCompletionSource и все объекты, на которые они ссылаются, в течение всего времени жизни наблюдателя (watcher). • RunContinuationsAsynchronously. Этот пункт заслуживает отдельного раздела. Где выполняется продолжение Когда вы завершаете TaskCompletionSource, код, ожидающий его выполнения, должен где-то запуститься. По умолчанию он выполняется в том же потоке, который вызвал SetResult, — непосредственно внутри вызова SetResult, до его завершения. А теперь представьте, что поток, выполняющий завершение операции, — это тот же поток, который вызывает обработчики FileSystemWatcher, обратный вызов сокета для получения данных или удерживает блокировку. Продолжение может представлять собой произвольный фрагмент кода вашего приложения. Оно выполняется в то время, когда блокировка уже захвачена или когда цикл обработки событий заблокирован. Если это продолжение само заблокируется или попытается захватить блокировку, которая уже удерживается потоком, выполняющим завершение, возникнет взаимоблокировка в месте, которое совершенно не указывает на истинную причину проблемы. Использование TaskCreationOptions.RunContinuationsAsynchronously решает эту проблему: метод SetResult ставит продолжение в очередь и немедленно возвращает управление. Предупреждение: Если у вас нет веских причин поступать иначе, всегда создавайте TaskCompletionSource с флагом TaskCreationOptions.RunContinuationsAsynchronously. Поведение по умолчанию сохранено ради обратной совместимости, а не потому, что оно является оптимальным. Завершённые задачи часто бесплатны Асинхронные методы, завершающиеся досрочно, встречаются часто, поэтому в BCL уделяется особое внимание тому, чтобы избежать выделения памяти в таких случаях: Метод Время Память CompletedTask 0.00ns - FromResult (кэшированное) 0.00ns - FromResult (частое значение) 0.00ns - FromResult (случайное значение) 2.95ns 72B TaskCompletionSourceSetResult 5.70ns 72B Task.CompletedTask — это синглтон. Вызов Task.FromResult(true) также практически бесплатен, поскольку среда выполнения кэширует объекты Task для небольшого набора часто используемых значений: true, false и малых целых чисел. А вот Task.FromResult(42) в этот набор не входит, поэтому при его вызове выделяется 72 байта памяти. Практический совет: если метод часто возвращает один и тот же результат, лучше кэшировать объект Task в статическом поле, а не вызывать Task.FromResult каждый раз. await или ContinueWith Метод ContinueWith использовался для добавления продолжения ещё до появления ключевого слова async, и его до сих пор можно встретить в старом коде. Он не эквивалентен await: Метод Время Память Await 5.56ns 72B ContinueWith 22.68ns 120B GetAwaiterGetResult 0.00ns - В 4 раза медленнее и «тяжелее» при работе с уже завершенной задачей. Но производительность — это еще не всё. Кроме того, ContinueWith: • По умолчанию выполняется в TaskScheduler.Current, а не в захваченном контексте, что неожиданное поведение по умолчанию для приложений с UI. • Возвращает Task<Task<T>>, если продолжение само является асинхронным — если, конечно, вы не вспомните про Unwrap. • Предоставляет исходную задачу вместо самого результата, поэтому при обращении к .Result исключения приходят в виде AggregateException, а не выбрасываются повторно. Отдельного упоминания заслуживает третья строка. Вызов GetAwaiter().GetResult() для уже завершённой задачи практически не требует затрат ресурсов и повторно выбрасывает исходное исключение, а не AggregateException. Это делает данный способ правильным для синхронного получения результата задачи, которая вы знаете, что уже завершена. Если же задача не завершена, этот метод блокирует поток, что влечёт за собой все последствия, описанные в первой статье этой серии. Способ ожидания влияет на то, как приходят исключения Это постоянно сбивает людей с толку: var task = Task.FromException( new InvalidOperationException("boom")); await task; // выбросит InvalidOperationException task.Wait(); _ = task.Result; // выбросят AggregateException, // оборачивающее InvalidOperationException await извлекает и повторно выбрасывает исходное исключение, сохраняя его трассировку стека. Wait и Result этого не делают, так как они появились раньше await, а задача (Task) может содержать несколько исключений — например, в случае Task.WhenAll, когда произошёл сбой сразу нескольких операций. Это ещё одна причина, по которой блок catch (Exception ex) вокруг вызова .Result часто перехватывает не то исключение, которое ожидалось. Источник: https://www.meziantou.net/task-taskcompletionsource-and-the-task-lifecycle.htm
Длинный пост
фото и оформление — в Telegram ↗
👍8
37 · 1.5K ·

Рядом в ленте

..NET РазработчикДень 2813. #ЗаметкиНаПолях #Async Ещё Раз Про Async/Await в C#. Часть 1 1. Почему существует асинхронность: цена блокировки потока Прежде чем углубиться в то, к..NET РазработчикДень 2814. #ЗаметкиНаПолях #Async Ещё Раз Про Async/Await в C#. Часть 2 Нет никакого потока В предыдущем посте было показано, что блокировка потока во время ожи
это сообщение
..NET РазработчикДень 2816. #Карьера #AI Станет ли ИИ-Кодинг Концом Программирования для Людей? Автор оригинала: Pavel Yosifovich Мое знакомство с компьютерами состоялось в 1983..NET РазработчикДень 2817. #ЧтоНовенького #VS Прекращаем Сборку После Первой Ошибки Мэдс Кристенсен продолжает поставлять небольшие и на первый взгляд очевидные улучшения проце
..NET Разработчик.NET Разработчик@NetDeveloperDiary · канал · Технологии
6 772подписчиков1 179средний охват поста
Лента площадки Открыть в Telegram

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

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