softhelp.ru! | статьи теметики - сеть и сайты


Принципы асинхронности на C#


Сравнение синхронных и асинхронных операций


Синхронная операция выполняет свою работу перед возвратом управления вызывающему коду. Асинхронная операция выполняет (большую часть или же всю) свою работу поем возврата управления вызывающему коду. Большинство методов, которые вы будете разрабатывать и вызывать, являются синхронными. В качестве примера можно привести Console .WriteLine или Thread. Sleep. Асинхронные методы менее распространены, и они инициируют параллелизм, т.к. их работа продолжается параллельно с вызывающим кодом. Асинхронные методы обычно быстро (или немедленно) возвращают управление вызывающему коду, поэтому их также называют неблокирующими методами.

Большинство асинхронных методов, которые мы видели до сих пор, могут быть описаны как универсальные методы:

• Thread.Start;

• Task.Run;

• методы, которые присоединяют признак продолжения к задачам.

В дополнение некоторые из методов, рассмотренных в разделе “Контексты синхронизации” (Dispatcher.Beginlnvoke, Control.Beginlnvoke и Synchronization Context. Post), являются асинхронными, как и методы, которые были написаны в разделе “TaskCompletionSource” ранее в главе, включая Delay.

Что собой представляет асинхронное программирование?



Принцип асинхронного программирования заключается в том, что длительно выполняющиеся (или потенциально длительно выполняющиеся) функции реализуются асинхронно. Это отличается от традиционного подхода синхронной реализации длительно выполняющихся функций с последующим их вызовом в новом потоке или в задаче для ввода параллелизма по мере необходимости.

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

• Параллельное выполнение с интенсивным вводом-выводом может быть реализовано без связывания потоков (как было продемонстрировано в разделе “TaskCompletionSource”), улучшая масштабируемость и эффективность.

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

Это, в свою очередь, приводит к двум различающимся применениям асинхронного программирования. Первое из них связано с написанием (обычно серверных) приложений, которые эффективно обрабатывают множество параллельных операций ввода-вывода. Проблемой здесь является достижение не безопасности потоков (поскольку разделяемое состояние обычно минимально), а эффективности потоков; в частности, отказ от потребления по одному потоку на сетевой запрос. Следовательно, в таком контексте выигрыш от асинхронности получают только операции с интенсивным вводом-выводом.

Второе использование касается упрощения поддержки безопасности потоков в обогащенных клиентских приложениях. Это особенно актуально при возрастании размеров программы, поскольку для борьбы со сложностью мы обычно проводим рефакторинг крупных методов в методы меньших размеров, получая в результате цепочки методов, которые вызывают друг друга (графы вызовов).

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

В случае графа асинхронных вызовов мы не должны запускать поток до тех пор, пока это не станет действительно необходимым — как правило, в нижней части графа (или вообще не запускать для операций с интенсивным вводом-выводом). Все остальные методы могут запускаться полностью в потоке пользовательского интерфейса со значительно упрощенными мерами по безопасности потоков. В результате получается мелкомодульный параллелизм — последовательность небольших параллельных операций, между которыми выполнение возвращается потоку пользовательского интерфейса.

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

Профили Metro и Silverlight .NET поддерживают асинхронное программирование даже там, где синхронные версии некоторых длительно выполняющихся методов вообще отсутствуют. Вместо этого доступны асинхронные методы, возвращающие задачи (или объекты, которые могут быть преобразованы в задачи посредством расширяющего метода As Task).

Асинхронное программирование и продолжение



Задачи идеально подходят для асинхронного программирования, т.к. они поддерживают признаки продолжения, которые являются существенно важными в реализации асинхронности. При написании Delay мы использовали класс TaskCompletionSource, предоставляющий стандартный способ реализации асинхронных методов с интенсивным вводом-выводом “нижнего уровня”.

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

Другой способ взглянуть на данную проблему заключается в том, что императивные конструкции циклов (for, foreach и т.п.) не очень хорошо смешиваются с признаками продолжения, поскольку они опираются на текущее локальное состояние метода (т.е. сколько еще раз этот цикл должен выполняться).

Хотя ключевые слова async и await предлагают одно решение, иногда возможно решить проблему другим путем, заменяя императивные конструкции циклов их функциональным эквивалентом (другими словами, запросами LINQ). Это основа инфраструктуры Reactive Framework (Rx) и может оказаться удачным вариантом, когда на результатах нужно выполнить операции запросов или скомбинировать несколько последовательностей. Недостаток состоит в том, что во избежание блокировки Rx оперирует на последовательностях с активным источником, которые концептуально сложны.