|
|
|||||||||||||||||||||||||||||
|
Шаблон разработки асинхронного программирования (исходники)Источник: rsdn Павлов Эдуард
По возможности в этой статье будут использоваться термины и обороты, которые используются в русской версии MSDN. Если по каким-то причинам термин, использующийся в MSDN, меня не устроит, я буду вводить свой термин, предварительно описав, что под ним подразумевается. В любом случае, при первом использовании переводного термина будет приводиться соответствующий английский термин.
Что такое шаблоны разработки асинхронного программирования и зачем они нужны?Приложениям время от времени нужно вызывать некоторую логику в отдельном потоке. В основном это делается для того, чтобы графический интерфейс пользователя (GUI) не "зависал" на время длительных вычислений или других операций. Теперь, с распространением многопроцессорных систем, можно извлечь пользу также и из распараллеливания вычислений. .NET Framework предоставляет все необходимое для создания многопоточных приложений, но еще в первой версии .NET Microsoft предложила к использованию шаблон асинхронного программирования - "асинхронные операции, использующие IAsyncResult" (http://msdn.microsoft.com/ru-ru/library/ms228963(en-us,VS.80).aspx). В этой статье данный шаблон будет называться более кратко - шаблон IAsyncResult. Для чего же нужны шаблоны, если библиотека классов .NET, как уже было сказано, предоставляет все необходимые классы? Как и любой шаблон, он привносит единообразие в решения похожих задач. Рассмотрим, для примера, классы делегатов и класс FileStream. Наряду с обычными методами эти классы предоставляют версии методов, которые возвращают управление сразу после начала операции, которая выполняется асинхронно. Далее в этой статье такие методы будут именоваться асинхронными методами, а операции, которые эти методы выполняют, соответственно асинхронными операциями. Так вот, все асинхронные методы этих классов устроены одинаковым образом, имеют схожие сигнатуры и идентичную семантику в том, что касается асинхронной работы - все в соответствии с шаблоном. Здесь надо отметить, что асинхронизация не всегда реализуется через вызов метода в контексте созданного в приложении потока. Вернемся к классу FileStream. Его асинхронные методы реализованы через "перекрывающийся ввод/вывод" (Overlapped I/O). То есть вызов, скажем, обычной версии метода Write в отдельном потоке не идентичен вызову его асинхронного аналога, BeginWrite. Write будет занимать вызывающий поток, пока не закончит работу, тогда как BeginWrite не только вернет управление немедленно, не дожидаясь окончания работы метода, но и будет выполняться в фоновом режиме, не создавая дополнительный поток в приложении. Дополнительный поток будет создан только тогда, когда асинхронная операция завершится, и нужно будет оповестить вызывающую сторону об этом и то, только в случае, если в BeginWrite был передан делегат метода обратного вызова. Выгоды использования асинхронных методов ввода/вывода очевидны. Дополнительные потоки если и будут создаваться, то только для обработки результата выполнения асинхронных операций, поэтому применение перекрывающегося ввода/вывода особенно ценно в серверных приложениях. Представим, что в некотором приложении есть графический интерфейс пользователя, который не должен "зависать" при выполнении длительных операций и, к примеру, сгенерированный автоматически класс-прокси доступа к Web-службе, предоставляющий асинхронные методы. На первый взгляд у нас есть все для выполнения нашей задачи, так как Web-служба предоставляет асинхронные методы, построенные по одному из предлагаемых Microsoft шаблонов (и реализованные через перекрывающий ввод/вывод), - знай, вызывай себе из слоя GUI асинхронные методы. Но в хорошо спроектированных системах и сам класс-прокси будет инкапсулирован в транспортном слое, да еще и между транспортным слоем и GUI будут лежать один или несколько слоев. Например, слой модели и слой представления, если мы воспользуемся шаблоном model-view-presenter. Таким образом, чтобы асинхронными операциями, предоставляемыми классом-прокси, можно было воспользоваться из слоя GUI, каждый слой должен реализовывать шаблон асинхронного программирования и предоставлять возможность выполнять операции асинхронно. Недостатки предлагаемых Microsoft шаблоновНа данный момент Microsoft предлагает два шаблона разработки для асинхронного программирования. Уже упоминавшийся шаблон IAsyncResult основан на интерфейсе IAsyncResult. Второй - "асинхронная модель, основанная на событиях" (Event-based Asynchronous Pattern) http://msdn.microsoft.com/ru-ru/library/wewwczdw.aspx. В дальнейшем он будет именоваться event-based-шаблон. Что же не устраивает в предлагаемых Microsoft шаблонах и для чего понадобилось создавать что-то свое? Давайте рассмотрим подробнее, что нам предлагает MS. Первым рассмотрим шаблон IAsyncResult. Вот его объявление с комментариями разработчиков:
Из комментариев видно, что этот интерфейс описывает абстракцию асинхронной операции, да и свойство IsCompleted присуще операции и не имеет отношения к результату. Почему интерфейс назван IAsyncResult - для меня загадка.
Классу-поставщику, в соответствии с этим шаблоном, предписывается иметь определенный интерфейс. На каждую асинхронную операцию объявляется по два метода - BeginOperationName и EndOperationName. Рассмотрим его использование на примере простейшего класса, предоставляющего всего одну асинхронную операцию, вычисляющую число Фибоначчи по его индексу.
Метод BeginCalculateFibonacciNumber начинает операцию вычисления числа Фибонначи и возвращает управление. По завершении операции будет вызван метод обратного вызова, переданный через аргумент callback (если он был передан). Для получения результата операции необходимо вызвать метод EndCalculateFibonacciNumber у того же экземпляра, у которого был вызван BeginCalculateFibonacciNumber. Вот простой пример использования класса NaiveMath.
В данном примере, чтобы вызвать метод EndCalculateFibonacciNumber, нужный экземпляр класса NaiveMath передается в метод BeginCalculateFibonacciNumber через аргумент state. Его можно сохранять в члене класса, но это не всегда удобно, и такой прием (передача в BeginOperationName) является обычной практикой при использовании шаблона IAsyncResult. Каковы же недостатки этого шаблона? В первую очередь - объявление метода для получения результата асинхронной операции, то есть EndOperationName, в классе-поставщике. Кроме абсолютно ненужного метода в интерфейсе класса, это приводит еще и к показанным в примере ужимкам для доступа к нужному экземпляру в методе обратного вызова. Но буду справедлив, скорее всего, наличие этого метода - жестокий пережиток отсутствия обобщений в .NET 1.x и сделан для типизации результата асинхронной операции. То есть в .NET 1.x этот метод был действительно нужен, но на данный момент утратил свой смысл. Все остальное - это не столько недостатки, сколько малый набор возможностей. Нет возможности прервать начатую операцию. Не предусмотрен механизм оповещения о прогрессе операции. При вызове асинхронного метода не обязательно указывать метод обратного вызова, это несомненный плюс, но если уж вы его передали, то отказаться от его вызова по завершении операции уже нельзя. Есть в этом шаблоне и очень удачное решение - это свойство AsyncWaitHandle интерфейса IAsyncResult, позволяющее вызывать методы WaitAny и WaitAll класса WaitHandle для множества асинхронных операций. Event-based-шаблон предоставляет перечисленные выше недостающие возможности, но имеет при этом свои недостатки. На недостатках этого шаблона я хотел бы остановиться более подробно по той причине, что на данный момент именно этот шаблон рекомендуется Microsoft для повсеместного использования. При этом, по моему мнению, он абсолютно не справляется с возложенной на него задачей. Реализуем класс NaiveMath в соответствии с event-based-шаблоном. Для оповещения потребителя о завершении асинхронной операции этот шаблон предполагает наличие события (event) типа:
То есть кроме самого класса, предоставляющего асинхронные операции, нам понадобятся объявления делегата и класса, наследующего AsyncCompletedEventArgs.
И если от объявления делегата можно избавиться, воспользовавшись делегатом Action<T1, T2>, появившимся в .NET 3.5, то класс CalculateFibonacciNumberCompletedEventArgs придется объявлять в любом случае. К тому же, Event-based-шаблон , так же, как и IAsyncResult, раздувает интерфейс класса. Кроме самого асинхронного метода, надо, как минимум, объявить событие, уведомляющее о его завершении. Приведенный пример реализации event-based-шаблона по возможностям идентичен примеру шаблона IAsyncResult, то есть не предоставляет возможности отмены асинхронной операции и не уведомляет потребителя о прогрессе исполнения операции. Это сделано сознательно, для корректности сравнения. Если же воспользоваться дополнительными возможностями event-based-шаблона, то в интерфейсе класса появятся еще один метод и одно событие (event). Таким образом, получается два-три открытых члена класса на одну асинхронную операцию. Хотя event-based-шаблон и предоставляет возможность прерывать асинхронные операции, но в этой области присутствуют странности. По логике приложения можно выделить две группы асинхронных методов. В одну группу отнесем такие методы, которые могут быть вызваны сколько угодно раз одновременно, то есть, не дожидаясь, пока предыдущий запущенный метод с таким же именем закончит свое выполнение.
В другой группе, соответственно, окажутся методы, которые по какой-то причине нельзя или не имеет смысла вызывать одновременно, например метод, запрашивающий некоторые статические данные с сервера.
Event-based-шаблон различает эти две группы операций. Различие заключается в сигнатуре методов. Реентерабельные методы, имеют дополнительный аргумент object userState - некий объект, уникально идентифицирующий асинхронную операцию. userState используется классом-потребителем, чтобы отличать операции друг от друга. Так же этот объект-идентификатор используется в методе void CancelAsync(object userState) поставщика, то есть реализация класса-поставщика идентифицирует эти операции по идентификаторам, переданным ей откуда-то снаружи. По моему мнению, в одном этом уже есть некоторый криминал. Но не это главное. У нереентерабельных методов аргумента userState нет, следовательно, такие операции не имеют идентификатора и не могут быть отменены!
В шаблоне IAsyncResult, в сигнатуре асинхронных методов, тоже используется аргумент userState, но его назначение совершенно иное. В шаблоне IAsyncResult этот аргумент используется для передачи контекста вызова асинхронного метода в метод обработки результата, и используется только классом-потребителем. В event-based-шаблоне нет такого понятия, как передача контекста вызова. Аргумент userState, несмотря на его название, используется только для идентификации вызовов реентерабельных методов. В принципе и понятно, смешивать в одном объекте идентификацию и контекст вызова опасно. Еще одним недостатком является отсутствие явно описанной абстракции "асинхронная операция". Чтобы узнать, что выполнение операции завершено, необходим экземпляр класса-поставщика, ведь именно в его интерфейсе объявлены соответствующие события. Таким образом, затруднено использование шаблона в случаях, когда асинхронные методы вызываются из одного класса, а ожидание их окончания и обработка результатов их работы осуществляется в другом. Конечно же, можно отдать в место обработки ссылку на экземпляр класса-поставщика, тем самым предоставив весь публичный интерфейс этого класса там, где нужна только информация об окончании работы асинхронных операций, усложнив контроль над временем жизни этого экземпляра и т.д. Но самым главным недостатком, на мой взгляд, является отсутствие контроля со стороны потребителя, в контексте какого потока будет вызвано уведомление о завершении операции. В "best practice" MS рекомендует основывать реализацию event-based-шаблона на функциональности классов AsyncOperation и AsyncOperationManager. В .NET методы элементов управления (UI controls) нельзя вызывать в контексте произвольного потока, а можно только в контексте того потока, в котором был создан дескриптор (handle) этого элемента управления - будем называть этот поток GUI-потоком. Для вызова произвольного метода в контексте этого потока используются методы Control.Invoke и Control.BeginInvoke. В .NET 2.0 появился новый класс SynchronizationContext, решающий задачу вызова методов в контексте определенного потока в общем случае, а не только для элементов управления. При создании экземпляра класса AsyncOperation запоминается текущий контекст синхронизации - экземпляр класса, наследующего SynchronizationContext (см. статическое свойство Current класса SynchronizationContext), и вызов метода обратного вызова по окончании асинхронной операции производится через запомненный экземпляр. Большинство вызовов методов в клиентской части приложения инициируются из слоя графического интерфейса пользователя, при этом в качестве текущего контекста синхронизации выставлен экземпляр класса WindowsFormsSynchronizationContext, наследника SynchronizationContext. То есть вызов метода обратного вызова, согласно идеологии event-based-шаблона, будет происходить в контексте GUI-потока. В реальных приложениях часто возникает необходимость вызвать одновременно несколько асинхронных методов и дождаться, когда все они закончат свое выполнение, чтобы продолжить обработку данных. При использовании шаблона IAsyncResult это делается путем вызова WaitHandle.WaitAll на массиве дескрипторов, доступных через свойство AsyncWaitHandle интерфейса IAsyncResult. Проиллюстрирую на примере.
Естественно, если какие-либо дальнейшие действия необходимо произвести в контексте какого-то определенного потока, потребитель сам должен позаботиться об этом. При использовании event-based-шаблона для достижения вышеописанного результата потребитель будет вынужден предоставить собственную реализацию класса SynchronizationContext. Эта особенность event-based-шаблона становится реальной головной болью в клиентских приложениях, взаимодействующих с Web-службами. Впрочем, MS допускает реализацию event-based-шаблона, не заботящуюся о том, в контексте какого потока вызывать оповещения. Но в любом случае, то, в контексте какого потока будут вызываться оповещения, зависит именно от реализации шаблона, а не от потребностей потребителя - это и есть самый большой недостаток. Забавно, что Microsoft рекомендует использовать шаблон IAsyncResult в ядре системы, на низких уровнях, а event-based-шаблон на верхних, предоставляемых клиенту. Что преследуется этой рекомендацией, мне, признаться, непонятно. Видимо Microsoft считает event-based-шаблон более удобным в использовании, но я не разделяю их мнение. Подведем итог, перечислим недостатки предлагаемых MS шаблонов. Шаблон IAsyncResult:
Event-based-шаблон:
Создание нового шаблонаИтак, перед нами стоит задача создать такой шаблон, который объединил бы в себе достоинства обоих предлагаемых MS шаблонов, по возможности избежав их недостатков. За основу я взял шаблон IAsyncResult, потому что он не страдает от системных проблем. По сути, у него всего два недостатка - два метода в интерфейсе класса-поставщика на одну операцию и недостаточная функциональность. Для начала введем возможность отменять операцию, но таким образом, чтобы не перегрузить интерфейс класса-поставщика. Для этого создадим необобщенный интерфейс, наследующий IAsyncResult.
Теперь перенесем метод получения результата операции из класса-поставщика в интерфейс операции. .NET версии 2.0 и выше позволяют сделать это, не отказываясь от строгой типизации.
Для того чтобы методами, связанными с прерыванием операции, можно было пользоваться из кода, не обладающего знаниями о типе результата операции, они помещены в необобщенный интерфейс. Применение такой возможности показано чуть ниже. Осталось определить тип делегата метода обратного вызова, и можно посмотреть шаблон в использовании. Он похож на AsyncCallback, используемый в шаблоне IAsyncResult, только принимает аргумент типа описанного выше интерфейса.
Теперь посмотрим, как будет выглядеть открытый интерфейс класса-поставщика на основе нашего шаблона.
Если сравнивать с аналогичным классом на основе шаблона IAsyncResult, исчез метод EndCalculateFibonacciNumber, что облегчило интерфейс класса, а BeginCalculateFibonacciNumber получил новое имя, заимствованное из event-based-шаблона, так как метод с именем, начинающимся с Begin при отсутствии соответствующего ему метода, начинающегося с End, нелогичен. А вот его использование (без каких бы то ни было проверок, только для иллюстрации).
Посмотрим, что изменилось по сравнению с аналогичным примером на основе шаблона IAsyncResult.
Введем еще одну возможность в шаблон и подведем промежуточный итог. Введем возможность отказываться от уведомления о завершении операции путем обратного вызова предоставленного метода.
Назначение и использование метода Abandon не очевидны, и я хотел бы раскрыть этот аспект подробнее. В подавляющем большинстве случаев (по крайней мере, на стороне клиента) вызов операций в не блокирующем режиме нужен для того, чтобы графический интерфейс пользователя не "зависал". В не блокирующем режиме запускаются потенциально долгие операции, это могут быть некоторые математические расчеты, как в приведенном выше примере или любая другая обработка данных. Но гораздо чаще встречается другой класс асинхронных операций - это операции, основанные на перекрывающемся вводе/выводе (overlapped I/O). Такие классы, как Socket и Stream, а также классы, на них основанные, например, прокси Web-служб, предоставляют асинхронные методы, основанные на перекрывающемся вводе/выводе. Итеративные алгоритмы можно прервать на любой итерации, а с операциями, основанными на перекрывающемся вводе/выводе, не все так просто. Рассмотрим для примера взаимодействие клиента и сервера через сеть на основе сокетов (socket). Клиент отправляет серверу некоторый запрос и ожидает ответа. Клиент может запросить некоторые данные с сервера, может запросить сервер изменить некоторые данные. И тот, и другой запросы клиент физически отменить не может, он может только отказаться от результатов выполнения запросов, не обрабатывать их. В случае запроса на получение данных отказ от результата равносилен отмене запроса, здесь все просто, можно использовать метод Cancel. Отказ же от обработки результата изменения данных не отменит самих изменений, то есть использование метода Cancel в данном случае внесет путаницу. Для чего вообще может понадобиться отказываться от результатов выполнения асинхронной операции? В основном это контроль за временем жизни экземпляра класса-потребителя. При вызове асинхронного метода потребитель отдает поставщику делегат, указывающий на один из своих методов (хотя это и не обязательно). Когда логическая жизнь потребителя заканчивается, например, когда пользователь закрывает соответствующую часть графического интерфейса пользователя, необходимо отписаться от всех событий во избежание утечек памяти и побочных эффектов, в том числе и от начатых асинхронных операций. Как уже упоминалось выше, шаблон IAsyncResult не обязывает потребителя передавать в асинхронный метод callback, но если он передан, то изменить эту ситуацию уже никак нельзя. Таким образом, метод Abandon шаблона AsyncOperation восполняет этот пробел, позволяя "отписаться" от уведомления о завершении асинхронной операции в любой момент. Теоретически, можно было наделить метод Cancel такой семантикой, что бы он отменял операцию, если ее можно отменить (запрос данных, итеративный алгоритм) и "отписывал" потребителя если отменить ее нельзя (запрос на изменение). Но такой подход мог бы только запутать использование метода Cancel и поэтому для отказа от результата, или если хотите, для "отписывания" от уведомления о завершении асинхронной операции был создан отдельный метод. Итак, что мы имеем на данный момент по сравнению с event-based-шаблоном.
То есть по присутствующей функциональности новый шаблон равен и даже превосходит event-based-шаблон. Но пока в нем не хватает некоторых удобств: контроля за тем, в контексте какого потока вызывать метод обратного вызова, и оповещения о прогрессе исполнения операции или о промежуточных результатах.
В общем и целом, то, что метод обратного вызова автоматически вызывается в контексте того потока, в котором был вызван асинхронный метод, может быть очень и очень полезно. Особенно в простейших случаях, которых, как водится, большинство. Event-based-шаблон в данном аспекте не устраивает исключительно потому, что решение, как это делать, зависит от реализации класса-поставщика, а не от запросов потребителя. Возможность выбирать, в контексте какого потока надо вызывать метод обратного вызова, надо предоставить потребителю. Для этого объявим следующий атрибут, которым можно будет помечать методы обратного вызова.
Если объявление метода обратного вызова помечено этим атрибутом, то он будет вызываться в контексте того потока, в контексте которого был вызван асинхронный метод. Точнее говоря, метод будет вызван через тот экземпляр класса, наследующего SynchronizationContext, который был доступен через свойство SynchronizationContext.Current на момент вызова асинхронного метода.
Настало время добавить в шаблон уведомление о прогрессе операции. Самым простым вариантом было бы объявить следующее событие в интерфейсе IAsyncOperation.
Но здесь есть две проблемы. Первая связана с самим событием. К тому моменту, когда объект, представляющий асинхронную операцию, будет доступен потребителю, и потребитель подпишется на это событие, операция может быть уже завершена. Вторая проблема касается информации, передаваемой в событии, - в случае ProgressChangedEventArgs это одно целочисленное значение. В большинстве случаев этого вполне достаточно, но бывают и случаи, когда необходима более подробная информация о прогрессе операции. Можно было бы ввести еще один аргумент обобщения для типа информации о прогрессе:
Но это сильно перегрузит интерфейсы как класса-поставщика, так и его потребителя. На практике подавляющее большинство операций не будет иметь оповещения о прогрессе вовсе. Таким образом, неразумно вводить информацию о типе аргумента оповещения о прогрессе во все операции, лучше четко выделить операции, оповещающие о прогрессе своего выполнения. То есть вместо вышеописанной иерархии интерфейсов использовать следующую:
Понадобится еще один тип делегата, который будет вызываться как по завершению операции, так и по мере выполнения операции:
Таким образом, сигнатура методов, начинающих операции, оповещающие о прогрессе своего выполнения, будет отличаться от методов, начинающих операции, не выполняющих такого оповещения.
Чтобы определить причину вызова метода обратного вызова, окончание операции или изменение состояния прогресса операции, используется свойство IsCompleted интерфейса IAsyncResult. Сравнение шаблоновИспользование классом потребителемТеперь, когда в шаблоне AsyncOperation есть вся необходимая функциональность, мы можем сравнить простоту реализации класса-поставщика и удобство его использования классом-потребителем. Начнем с использования, как более практически важного аспекта. Для этого создадим более сложный класс-поставщик, чем использовали ранее. Вот как будет выглядеть открытый интерфейс класса-поставщика, реализованного согласно шаблону AsyncOperation. Он очень лаконичен, ничего лишнего, всего три метода, по одному на каждую операцию.
Рассмотрим введенные методы подробней. CalculateGDCAsync - вычисляет наибольший общий делитель для двух целых чисел. Как видно из сигнатуры метода, он не уведомляет потребителя о прогрессе операции. Также, в интересах науки, будем считать его нереентерабельным. Операция может быть прервана. CalculateFibonacciNumberAsync - этот метод, как и в предыдущих примерах, вычисляет число Фибоначчи по индексу. Оповещает о прогрессе операции, используя значение типа int от 0 до 100 %%. Операция может быть прервана. CalculateExponentaByTeilorSeriesAsync - вычисляет экспоненту методом разложения функции в ряд Тейлора. Такой странный метод понадобился, чтобы проиллюстрировать прерывание итеративного алгоритма на некоторой итерации. Операция может быть прервана. Теперь этот же класс, реализованный в соответствии с event-based-шаблоном.
Открытый интерфейс класса содержит гораздо больше членов, чем при использовании шаблона AsyncOperation. Но это еще не все. В качестве типов делегатов при определении событий используется обобщенная версия делегата Action, поэтому дополнительных типов делегатов объявлять не придется, но в любом случае понадобятся классы, наследующие AsyncCompletedEventArgs.
В примере класса на основе event-based-шаблона используются нововведения C# 3.5 для уменьшения количества кода. Но, несмотря на это, первый раунд event-based-шаблон проиграл вчистую. Второй раунд - использование класса-поставщика потребителем.
Первым рассмотрим использование метода CalculateGCDAsync, реализованного в соответствии с шаблоном AsyncOperation.
Этот пример иллюстрирует вызов асинхронного метода, обработку результата по окончании его работы и прерывание исполнения асинхронной операции по запросу пользователя. Также здесь используется прием передачи контекста вызова в метод обработки результата через аргумент "state" асинхронного метода. Теперь то же самое для event-based-шаблона.
Начнем с вызова асинхронного метода. Прежде чем вызывать асинхронный метод, надо подписаться на соответствующее событие, соответственно, в методе обработки результата надо отписаться от него, а для этого необходим доступ к экземпляру класса-поставщика. Пришлось хранить его как член класса. Как уже говорилось выше, event-based-шаблон не предоставляет унифицированного механизма передачи контекста вызова. Если мы хотим в случае отсутствия НОД предоставить клиенту развернутое оповещение (см. блок обработки исключительной ситуации в методе-обработчике результата в примере для шаблона AsyncOperation), включающее числа, для которых производилось вычисление, мы будем вынуждены сохранять и эти числа как члены класса. Можно заметить, что эти числа можно передать в метод обработки результата посредством класса CalculateGCDCompletedEventArgs. Но это будет не передача контекста, а жестко закодированная возможность. К тому же эта возможность потенциально избыточна в большинстве случаев, потому что контекст выполнения определяется потребностями класса-потребителя, тогда как класс CalculateGCDCompletedEventArgs входит в контракт класса-поставщика. Теоретически, мы могли бы воспользоваться аргументом userState, который используется в event-base шаблоне для идентификации асинхронных операций, для передачи контекста вызова. Например, создав класс, который бы гарантировал надежную идентификацию и позволял передавать дополнительные данные. Но в данном случае мы определили метод CalculateGCDAsync как нереентерабельный, а у таких методов, согласно идеологии event-based-шаблона, нет аргумента userState. Ну и в качестве финального аккорда, так как этот метод нереентерабельный, то его нельзя прервать. Подведем итог данного примера. В шаблоне AsyncOperation есть изящный способ передачи контекста вызова в метод обработки результата. На практике эта возможность очень часто востребована, что будет показано ниже. В event-based-шаблоне такая возможность отсутствует, что на практике оборачивается головной болью и постоянным применением обходных путей. Также в event-based-шаблоне, по не известным науке причинам, нет возможности прервать выполнение нереентерабельной операции. В простейшем примере event-based-шаблон показал свою несостоятельность сразу по нескольким параметрам, так что и этот раунд остался за шаблоном AsyncOperation. В следующем примере мы рассмотрим ожидание завершения нескольких операций для продолжения работы. По логике приложения необходимо запустить несколько асинхронных операций, дальнейшую же работу производить только когда все операции вернут результат. В данном примере будут вычисляться сразу три числа Фибоначчи. Вот как это реализуется с помощью шаблона AsyncOperation.
Вызывая метод CalculateFibonacciNumberAsync для первых двух индексов, мы вообще не указываем метод обработки результата, вместо этого сохраняя возвращенные методом дескрипторы асинхронных операций. При вызове метода CalculateFibonacciNumberAsync для третьего индекса, мы указываем метод On_CalculateFibonacciNumberCompleted как метод обработки результата, а два дескриптора операций передаем через аргумент state. Таким образом, они будут нам доступны в методе On_CalculateFibonacciNumberCompleted, для того чтобы дождаться окончания их выполнения. Метод On_CalculateFibonacciNumberCompleted не помечен атрибутом [SynchronizedCallback] и, следовательно, будет вызван не в контексте GUI-потока и не заблокирует графический интерфейс пользователя, пока мы ожидаем окончания выполнения остальных операций. Правда, при этом приходится заботиться о том, чтобы вызывать методы View в контексте нужного потока, что отображено вызовом метода View.Invoke в псевдокоде. Попробуем реализовать то же самое с помощью event-based-шаблона. Как уже упоминалось выше, event-based-шаблон предполагает, что методы-обработчики событий автоматически вызываются в контексте того потока, из которого был вызван метод CalculateFibonacciNumberAsync, то есть в нашем случае в контексте GUI-потока. Следовательно, мы не можем использовать простую схему, использованную выше. То есть, мы не можем при обработке результата одной из операций, с помощью каких-либо событий, дождаться окончания двух других, потому что случится не просто зависание графического интерфейса пользователя, а взаимоблокировка. Поэтому приходится использовать менее тривиальные способы, например, такой:
Можно придумать еще несколько способов добиться того же результата, но все они будут лишь обходными маневрами. На самом деле, в event-based-шаблоне есть целый комплекс недостатков, вытекающих из одной единственной причины - отсутствия четко определенной абстракции асинхронной операции - то, что в шаблоне AsyncOperation представлено интерфейсом IAsyncOperation. Именно наличие такой абстракции сильно упрощает жизнь в большом количестве случаев. Взять тот же часто применяемый метод опроса состояний операций (polling). Вот так он будет реализован при помощи шаблона AsyncOperation:
Или так.
Чтобы реализовать нечто подобное при помощи event-based-шаблона, придется вводить абстракцию операции самостоятельно, в отрыве от шаблона. В шаблоне AsyncOperation операция централизовано предоставлена самим шаблоном и глубоко с ним интегрирована. Счет 3:0, двигаемся дальше. Пришло время посмотреть, как пользоваться оповещением о прогрессе исполнения операции. Для иллюстрации будет использован метод, вычисляющий значение экспоненциальной функции методом рядов Тейлора. Столь экзотичный метод был выбран потому, что в этом методе от количества итераций зависит точность вычислений. Проще говоря, данный метод на каждой итерации дает "корректный" промежуточный результат с некоторой точностью. Как обычно, сначала пример реализации с шаблоном AsyncOperation.
И для обработки окончательного результата, и для обработки прогресса исполнения/промежуточного результата используется один и тот же метод-обработчик. Чтобы понять, по какой причине он вызван, используется свойство IsCompleted интерфейса IAsyncResult. В качестве аргумента метод-обработчик принимает перегрузку интерфейса IAsyncOperation с двумя аргументами обобщения. Этот интерфейс, кроме прочего, содержит свойство BreakOperation булева типа. Если в обработчике присвоить этому свойству значение true, то исполнение асинхронной операции будет прервано на этой итерации, что и проиллюстрировано в данном примере. Теперь то же самое для event-based-шаблона .
Начнем по порядку. Если операция уведомляет о прогрессе выполнения, при вызове метода приходится подписываться уже не на одно, а на два события. Далее, чтобы прервать операцию на некоторой итерации, нужно вызвать метод CancelAsync класса-поставщика. При этом нет гарантии, что алгоритм прервется именно на этой итерации, вполне возможно, что, пока отрабатывает метод CancelAsync, от поставщика придет еще одно оповещение о прогрессе и такой случай придется обрабатывать отдельно. Приходится постоянно заботиться о доступе к экземпляру класса-поставщика, чтобы контролировать количество подписок на события. Кроме того, для того чтобы операцию можно было отменить, нам необходим идентификатор операции. Так как он используется в двух методах, его тоже пришлось хранить как член класса. Во всех сценариях использования шаблонов классом-потребителем шаблон AsyncOperation на голову превзошел своего конкурента от Microsoft. Возможно, код, иллюстрирующий применение event-based-шаблона , выглядит слегка небрежным, а решения - не оптимальными. Но это не от того, что я специально ставил event-based-шаблон в невыгодное положение. Основная проблема иллюстрирования применения event-based-шаблона заключается в том, что для реализации всего "по уму" придется написать много дополнительного кода. Кроме того, реализация будет очень сильно зависеть от деталей взаимодействия класса-поставщика со своим клиентом, то есть View. При применении шаблона AsyncOperation, напротив, код получается слабо связанным с деталями реализации и зависит только от бизнес-логики. Достигается это тем, что взаимодействие с классом-поставщиком сведено к минимуму, а так же возможностью элегантно передавать контекст вызова асинхронного метода в метод (или место) обработки результата операции. По сути, класс-поставщик используется только для создания экземпляра операции, дальнейшее же общение происходит именно с этим экземпляром. Вообще говоря, использование шаблона AsyncOperation потребителем мало чем отличается от использования шаблона IAsyncResult, так что любой, кто пользовался шаблоном IAsyncResult, с легкостью сможет освоиться и с шаблоном AsyncOperation. Использование классом поставщикомРассмотрим теперь реализацию классов-поставщиков. Начнем, как обычно, с шаблона AsyncOperation. Код сопровожден поясняющими комментариями.
Как видите, весь код, относящийся к реализации шаблона AsyncOperation, сводится к созданию экземпляра класса AsyncOperation<TResult> и вызову метода SetAsCompleted по окончанию вычислений. Реализация на основе event-based-шаблона мало чем отличается в этом плане.
Из отличий можно отметить необходимость создания экземпляров классов-наследников AsyncCompletedEventArgs, а также то, что метод PostOperationCompleted принимает делегат типа SendOrPostCallback. Это не позволяет передать в него непосредственно событие, а вынуждает создать дополнительный метод или воспользоваться анонимным методом. Но за исключением этих мелочей код идентичен. Все сводится к созданию некоторого объекта в начале операции и вызову метода обработки результата, предоставленного потребителем в конце. Ну и надо помнить, что в event-based-шаблоне нельзя прервать нереентерабельную операцию, поэтому код прерывания в примере отсутствует. Теперь посмотрим на реализацию реентерабельных методов, которые можно прервать. Шаблон AsyncOperation:
Event-based-шаблон:
Из-за отсутствия возможности хранения контекста вызова в самой операции (в данном случае контекста поставщика), связывание операции и дополнительных данных приходится производить с помощью поля типа Dictionary. Это не только добавляет поле, но и заставляет заботиться о потокобезопасном доступе к нему. В целом, хотя реализация поставщиков на основе обоих шаблонов очень схожа, все же надо отметить, что код на основе шаблона AsyncOperation более чист и лаконичен. Я старался сделать таким и код на основе event-based-шаблона. Как выглядит класс, предоставляющий всего одну асинхронную операцию в примере, предлагаемом Microsoft для подражания, вы можете ознакомиться по этой ссылке http://msdn.microsoft.com/ru-ru/library/9hk12d4y.aspx РеализацияРеализация шаблона AsyncOperation состоит из двух классов - реализации интерфейсов IAsyncOperation<TResult> и IAsyncOperation<TResult, TIntermediateResult>. Код этих классов достаточно тривиален и сопровожден подробными комментариями. Так что в статье не будет подробного разбора реализации, но несколько слов о том, почему были приняты те или иные решения, все-таки необходимо сказать. Рассмотрим подробнее процесс прерывания операции. Первое решение, которое необходимо принять - нужно ли оповещать потребителя, что операция прервана или достаточно вызвать метод Cancel и забыть об этой операции. С одной стороны, напрашивается решение "отменил и забыл", но с другой, логика приложений бывает разной, в том числе и достаточно сложной. К примеру, операция может быть прервана "третьим" по отношению к потребителю, начавшему операцию, "лицом". В таком случае было бы желательно, чтобы потребитель был оповещен о том, что операция была прервана. Или другой, более вероятный вариант - необходимо отображать в GUI процесс отмены операции (отобразить что-то наподобие "Отмена" с песочными часами и заблокировать соответствующие элементы управления), дождаться, когда операция прервется, и привести состояние графического интерфейса пользователя в соответствие с состоянием модели. На основании этих доводов было принято решение оповещать потребителя о прерывании операции. Теперь надо продумать детали наиболее гибкой и удобной реализации этого. Первое, что приходит в голову - оповещать через тот же метод обратного вызова, через который происходит оповещение об окончании операции. В этом подходе есть одна сложность: если метод обратного вызова не был передан при начале операции, то оповещать о ее прерывании будет не через что. Есть еще один вариант - передавать метод обратного вызова в метод Cancel, но при таком подходе не реализуется сценарий оповещения потребителя, если операцию прервала третья сторона. Таким образом, был принят первый вариант. Что же касается вышеописанной сложности, то если потребитель при начале операции не передал метод обратного вызова, это означает, что он осуществляет контроль над операцией другим способом, например, методом опроса (polling), либо что его вовсе не интересует, когда операция будет завершена. К тому же, для того, чтобы вызвать метод Cancel, необходимо иметь ссылку на экземпляр операции, а значит, у потребителя остается способ определить момент, когда операция была прервана. То есть описанная проблема проблемой не является. Бывают и обратные ситуации, когда метод обратного вызова передан, а оповещение о прерывании операции потребителя не интересует. Можно завести перегрузку метода Cancel с параметром, например, так:
или даже завести дополнительный метод
Но в данной реализации было принято решение не делать этого, дабы не плодить сущности, а в случае необходимости вызывать метод Abandon перед вызовом Cancel. Что же касается оповещения о прогрессе операции, то надо признаться, что данная функциональность не обкатана достаточным образом на реальных приложениях по той простой причине, что она очень редко востребована. Например, решение вызывать метод обратного вызова для оповещения о прогрессе операции в блокирующем режиме может вызывать сомнения. Сделано это было для того чтобы была возможность прервать итеративный алгоритм на определенной итерации. Ведь если оповещать о прогрессе асинхронно, то к моменту, когда поставщик обработает свойство BreakOperation, потребителю может быть отправлено еще не одно оповещение. Насколько такой подход удобен, может показать только практика. Также стоит отметить, что в реализации шаблона не используются возможности C# и .NET выше второй версии. ЗаключениеЦелью данной работы являлось создание шаблона разработки асинхронного программирования, превосходящего по потребительским качествам шаблоны, предлагаемые Microsoft. Поэтому в качестве заключения будет приведена сравнительная таблица шаблонов.
Практика использования шаблона AsyncOperation показала его удобство, гибкость и мощь, особенно в случаях, когда каждый слой приложения должен реализовать шаблон асинхронного программирования. Если даже по какой-то причине, вы не можете использовать его вместо event-based-шаблона, им всегда можно заменить устаревший шаблон IAsyncResult, который Microsoft рекомендует для использования в "классах нижнего уровня", а также в приложениях, критичных к производительности (http://msdn.microsoft.com/ru-ru/library/ms228966(en-us,VS.80).aspx). Ссылки по теме
|
|