Найти в Дзене
using Dev

Как на самом деле работает Async/Await в C# Ч.2

Асинхронный шаблон на основе событий

В .NET Framework 2.0 появилось несколько API, которые реализовали другой шаблон для обработки асинхронных операций, в первую очередь предназначенный для этого в контексте клиентских приложений. Этот асинхронный шаблон на основе событий, или EAP( Event-Based Pattern), также представлен в виде пары элементов (по крайней мере, возможно, больше), на этот раз метода для инициирования асинхронной операции и события для прослушивания ее завершения. Таким образом, наш предыдущий DoStuff пример мог быть представлен в виде набора элементов, подобных этому:

-2

Вы бы зарегистрировали свою работу с продолжением в DoStuffCompleted событии, а затем вызвали DoStuffAsync метод; он инициировал бы операцию, и по завершении этой операции DoStuffCompleted событие было бы вызвано асинхронно от вызывающей стороны. Затем обработчик может запустить свою работу продолжения, вероятно, проверяя, что userToken предоставленное совпадение соответствует тому, которое он ожидал, позволяя нескольким обработчикам подключаться к событию одновременно.

Этот шаблон немного упростил несколько вариантов использования, в то же время значительно усложнив другие варианты использования (и, учитывая предыдущий пример APM CopyStreamToStream). Он не получил широкого распространения и эффективно появился в одном выпуске .NET Framework, хотя и оставил после себя API, добавленные во время его использования, такие как Ping.SendAsync/Ping.PingCompleted:

-3

Тем не менее, это добавило одно заметное преимущество, которое шаблон APM вообще не учитывал, и которое сохранилось в моделях, которые мы используем сегодня: SynchronizationContext.

SynchronizationContext также был представлен в .NET Framework 2.0 как абстракция для общего планировщика. В частности, SynchronizationContext наиболее часто используемым методом является Post, который помещает рабочий элемент в очередь на любой планировщик, представленный этим контекстом. Базовая реализация SynchronizationContext, например, просто представляет ThreadPool, и поэтому базовая реализация SynchronizationContext.Post просто делегирует ThreadPool.QueueUserWorkItem, которая используется, чтобы попросить ThreadPool вызвать предоставленный обратный вызов с соответствующим состоянием в одном из потоков пула. Однако суть SynchronizationContext заключается не только в поддержке произвольных планировщиков, скорее речь идет о поддержке планирования таким образом, чтобы оно работало в соответствии с потребностями различных моделей приложений.

Рассмотрим фреймворк пользовательского интерфейса, такой как Windows Forms. Как и в большинстве фреймворков пользовательского интерфейса в Windows, элементы управления связаны с определенным потоком, и этот поток запускает перекачку сообщений, которая выполняет работу, способную взаимодействовать с этими элементами управления: только этот поток должен пытаться манипулировать этими элементами управления, и любой другой поток, который хочет взаимодействовать с элементами управления, должен делать это, отправляя сообщение, которое будет использовано перекачкой потока пользовательского интерфейса. Windows Forms упрощает это с помощью таких методов, как Control.BeginInvoke, которые ставят в очередь предоставленный делегат и аргументы для запуска любым потоком, связанным с этим Control. Таким образом, вы можете написать код, подобный этому:

-4

Это разгрузит ComputeMessage() работу, которая должна быть выполнена в ThreadPool потоке (чтобы поддерживать адаптивность пользовательского интерфейса во время его обработки), а затем, когда эта работа будет завершена, поставит делегата в очередь обратно в связанный с потоком поток для button1 обновления button1 метки. Достаточно просто. В WPF есть нечто подобное, только с его Dispatcher типом:

-5

И в .NET MAUI есть нечто подобное. Но что, если бы я захотел поместить эту логику во вспомогательный метод? например:

-6

Затем я мог бы использовать это следующим образом:

-7

но как можно ComputeMessageAndInvokeUpdate реализовать его таким образом, чтобы он мог работать в любом из этих приложений? Нужно ли его жестко кодировать, чтобы знать о каждой возможной платформе пользовательского интерфейса? Вот где SynchronizationContext хорош. Мы могли бы реализовать метод следующим образом:

-8

Который использует SynchronizationContext как абстракцию для нацеливания на любой "планировщик”, который следует использовать, чтобы вернуться к необходимой среде для взаимодействия с пользовательским интерфейсом. Затем каждая модель приложения гарантирует, что она публикуется как SynchronizationContext.Current SynchronizationContext производный тип, который выполняет “правильные действия”. Например, в Windows Forms есть это:

-9

и в WPF есть это:

-10

ASP.NET у раньше был один, который на самом деле не заботился о том, в каком потоке выполняется работа, а скорее о том, что работа, связанная с данным запросом, была сериализована таким образом, что несколько потоков не могли одновременно обращаться к данному HttpContext:

-11

Это также не ограничивается такими основными моделями приложений. Например, xunit - популярная платформа модульного тестирования, которая используются в .Net Core для модульного тестирования, а также используют несколько пользовательских SynchronizationContextсистем. Вы можете, например, разрешить параллельное выполнение тестов, но ограничить количество тестов, которым разрешено выполняться одновременно. Как это включить? С помощью SynchronizationContext:

-12

MaxConcurrencySyncContextс Post метод просто помещает работу в свою собственную внутреннюю рабочую очередь, которую он затем обрабатывает в своих собственных рабочих потоках, где он контролирует, сколько их существует, исходя из желаемого максимального параллелизма.

Как это связано с асинхронным шаблоном на основе событий? Оба EAP и SynchronizationContext были введены одновременно, и EAP диктовал, что события завершения должны ставиться в очередь до любого SynchronizationContext был актуальным на момент запуска асинхронной операции. Чтобы немного упростить это (и, возможно, недостаточно, чтобы оправдать дополнительную сложность), некоторые вспомогательные типы были также представлены в System.ComponentModel, в частности AsyncOperation и AsyncOperationManager. Первый был просто кортежем, который упаковывал предоставленный пользователем объект состояния и захваченный SynchronizationContext, а второй служил простой фабрикой для выполнения этого захвата и создания экземпляра AsyncOperation . Тогда реализации EAP использовали бы их, например Ping.SendAsync called AsyncOperationManager.CreateOperation для захвата SynchronizationContext, а затем, когда операция завершена, метод AsyncOperationPostOperationCompleted‘s был бы вызван для вызова метода stored SynchronizationContext Post .

SynchronizationContext предоставляет еще несколько безделушек, достойных упоминания, поскольку они появятся снова через некоторое время. В частности, он раскрывает методы OperationStarted и OperationCompleted . Базовая реализация этих виртуальных машин пуста и ничего не делает, но производная реализация может переопределить их, чтобы узнать об операциях в полете. Это означает, что реализации EAP также будут вызывать эти OperationStarted/OperationCompleted в начале и конце каждой операции, чтобы информировать любого присутствующего SynchronizationContext и позволить ему отслеживать работу. Это особенно актуально для шаблона EAP, потому что методы, которые инициируют асинхронные операции, являются void возвращаясь: вы не получаете обратно ничего, что позволило бы вам отслеживать работу по отдельности. Мы вернемся к этому.

Итак, нам нужно было что-то получше шаблона APM, и следующий EAP представил несколько новых вещей, но на самом деле не решал основные проблемы, с которыми мы столкнулись. Нам все еще нужно было что-то получше.

Источник to be continued...