О статье

Канал статтей


Длительный Активный Объект. Часть 1.

Предисловие. Как-то передо мной стала задача загрузки файла с клиентского приложения на сервер приложений. Размеры файлов могли варьировать и при этом могла возникнуть ситуация, когда сервис мог отвергнуть файл из-за превышения максимального размера данных или возникнуть тайм-аут. Также клиентское приложение в момент передачи данных должно было обрабатывать другие более важные события, такие как обработка других более важных событий сервера приложений, отрисовка пользовательского интерфейса и так далее. Безусловно были важны функции отмены операции и отображения прогресса выполнения. Также было важно при ошибке продолжить передачу не с начала, а с того места, где произошла ошибка.

пример реализации
Рис. 1. Пример решения подобной задачи в одном известном приложении.

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

Решение. Для решения этой и других подобных задач как никогда подходит шаблон Длительного Активного Объекта (Long Running Active Object).

Структура. Активные объекты в принципе такие же, как и обычные (пассивные) объекты. Они также имеют поля и методы (рисунок 2).

пассивный объект
Рис. 2. Пассивный Объект

Отличие составляет то, что методы активного объекта выполняются в другом потоке, отличном от основного потока вызывающего объекта, клиента (Client), то есть методы выполняются асинхронно (asynchronous execution). Активный объект имеет внешний интерфейс (public interface) и внутреннюю реализацию. Внешний интерфейс - это так называемый посредник (Proxy). Он доступен для клиента, отвечает за вызовы методов активного объекта и в идеале имеет такой же интерфейс как и пассивный объект. Посредник преобразовывает вызовы методов в сообщения (Messages), помещает их в очередь выполнения (Message Queue) и возвращает ссылку на будущий результат выполнения клиенту в виде объекта (Future). Сообщение содержит всю информацию, необходимую для выполнения реального метода, приоритет задачи и другое. Специальный объект - диспетчер (Dispatcher) или планировщик (Scheduler) по определенному закону (например, приоритету), по-одному изымает сообщения из очереди и выполняет метод слуги(Servant). Слуга и есть реальный метод для выполнения или по-другому метод пассивного объекта, который использовался до применения шаблона активного объекта. Так как методы слуги выполняются в одном потоке последовательно, при этом отсутствует конкурентный доступ и не требуются дополнительные меры (блокировки) как при работе с ресурсами в многопоточных приложениях. Результат выполнения метода слуги помещается в Future объект, ссылка на который ранее была возвращена клиенту (рисунок 3).

активный объект
Рис. 3. Активный Объект

Клиент в любой момент может попытаться получить результат выполнения с Future объекта. Если результат ещё не был вычислен, тогда клиент будет вынужден приостановить выполнения кода и дождаться результата выполнения активного объекта. Как альтернативным является случай, когда при выполнении метода слуги произошла ошибка. В таком случае Future объект получит статус ошибки и полную информацию об исключении. Клиент может попытаться отменить выполнение метода. Если это произойдет до обработки сообщения диспетчером, тогда диспетчер отменит вызов метода слуги и установит состояние Future объекта как аннулированное. Все возможные состояния Future показаны на рисунке 4.

состояния future объекта
Рис. 4. Состояния Future объекта

Динамика работы. Перед началом работы с активным объектом необходимо создать диспетчер или планировщик. Диспетчеру для работы с очередью сообщений необходим алгоритм (strategy) получения следующего сообщения для обработки. Этот алгоритм можно задать как до запуска диспетчера, так и при его работе. Диспетчер вместе с посредником реализует шаблон “производитель-потребитель” (Producer-Consumer). Диспетчер делит с посредником очередь сообщений и обрабатывает поступившие туда сообщения. Обработка сообщений происходит в отдельном (рабочем) потоке, который необходимо будет создать и при уничтожении диспетчера (Dispose) также уничтожить. Работу диспетчера можно так же приостановить и потом возобновить обратно. На рисунке 5 показана динамика работы диспетчера.

динамика работы диспетчера
Рис. 5. Динамика работы диспетчера

Когда в очередь попадает одно или несколько сообщений, диспетчер запускает рабочий поток для обработки сообщений. Если сообщений нет в очереди, тогда рабочий поток переходит в режим ожидания. Диспетчер, согласно алгоритма выборки, получает первое сообщение и вызывает метод этого сообщения AllowInvoke(), который разрешает или запрещает выполнение метода Слуги. Таким образом реализован механизм защиты для безопасного вызова метода слуги. Если в данный момент невозможно обработать сообщение, диспетчер выбирает следующее из очереди. Если защитник (Guard) разрешил выполнение, тогда диспетчер вызывает метод сообщения Invoke(). Методы сообщения AllowInvoke() и Invoke() используют слугу (Servant) и его реальные методы. В случае ошибки выполнения метода Слуги, будет вызван другой метод сообщения Failed(), который установит состояние будущего результата (Future) в состояние ошибки или будет вызван другой метод Слуги для обработки исключения. В случае удачного выполнения метода Invoke(), диспетчер установит состояние в удачное (Success) и присвоит результат выполнения этого метода в объекте Future. Так же возможен вариант, когда выполнение сообщения может быть отменено до момента обработки этого сообщения диспетчером (состояние Canceled). После того как диспетчер будет установлен, можно выполнять методы активного объекта.

Для решения конкретной задачи, а именно загрузки (upload) файла неопределенной длины на сервер приложений в фоновом режиме (background) с возможностью отмены этой операции были разработаны следующие классы: Класс ProcessFileByPiecesCommand – это слуга (Servant), реализация интерфейса IServant. Основная задача этого класса порционно читать равные части файла. После отсылки порции данных на сервер приложений (эта реализация опущена в данном примере), автоматически посредник создает и ставит в очередь следующее сообщение. Такой процесс происходит до тех пор, пока не будут считаны и отосланы все части файла или процесс не закончится по причине отмены задачи пользователем или ошибке. Класс ActiveObjectProxyServant – это класс-адаптер для работы с объектами, реализующими IServant, как с активными объектами. Реализация посредника в чистом виде отсутствует (возможно использование Dynamic Proxy от Castle Project, других фрейворков или создание класс посредника). Этот адаптер может быть вызван методами посредника. Адаптер содержит список результатов выполнения. Может получиться так, что результат выполнения процесса загрузки будет состоять из результатов выполнения загрузки каждой части файла. Один и тот же адаптер может быть использован для копирования другого файла, третьего и так далее. Для этого адаптер и использует список результатов вместе с промежуточными результатами по загрузке частей файла. Класс ActiveScheduler – это реализация диспетчера (IDispatcher) для обработки сообщений. Диспетчер может обрабатывать и другие сообщения системы. Например, сообщения по загрузке файла имеют низкий приоритет и если в очередь сообщений поступит сообщение с большим приоритетом, тогда оно будет обработано диспетчером в первую очередь. Таким образом система будет обрабатывать критические задачи в первую очередь и лишь потом низкоуровневые. Класс PriorityAndLIFO – класс реализации IQueueSortStrategy, алгоритм выборки сообщений согласно приоритета сообщения и его номера по-порядку в очереди. Класс Future и его наследники - для получения и хранения состояния и результата выполнения метода активного объекта. Класс ServantOperation – это класс реализации сообщения (IOperation, Operation), носитель информации об операции и результате выполнения. Динамика работы активного объекта показана на рисунке 6.

динамика работы активного объекта
Рис. 6. Динамика работы Активного Объекта по загрузке файла на сервер приложений

Другие шаблоны. Кроме шаблона Активный Объект (Active Object), также были использованы следующие шаблоны проектирования (design patterns): Стратегия (Strategy), Фабричный метод (Factory method), шаблон для предотвращения ошибок на ранней стадии разработки (Assert, Assertion, “Fail Fast”), шаблон Команда (Command), Посредник (Proxy)

Используемые возможности и техники. При разработке были использованы следующие возможности .NET Framework и в частности версии 3.5: .NET рефлекшн (.NET reflection), специальные аттрибуты (custom attributes), перегрузка операторов (operators overloading), мультипоточность, блокировки и конкурентный доступ к ресурсам (multithreading, lock, ReaderWriterLockSlim, Monitor), и конечно же мощные средства объектно-ориентированного программирования (OOP): наследование, инкапсуляция и полиморфизм. Также использовались юнит тесты (NUnit) и документирование кода (аттрибуты любого хорошего кода).

назад
Загрузить код

Необходимые условия: .NET Framework 3.5, Visual Studio 2008, NUnit