Часто при создании приложений разработчики ограничиваются выделением бизнес-логики в отдельный слой. Но когда бизнес-логики становится много, она своей активностью начинает негативно влиять на плавность пользовательского интерфейса.
Спикер поделится разработанным им подходом, позволяющим аккуратно выделить бизнес-логику из главного потока, не требуя при этом от объектов данного слоя быть потокобезопасными.
Сергей Николенко, Deloitte Analytics Institute, Высшая Школа Экономики, «От н...
Similar to Введение в паттерн Schedulable object, Павел Осипов, руководитель разработки iOS-приложений Облака Mail.Ru, преподаватель Технопарка Mail.Ru
Similar to Введение в паттерн Schedulable object, Павел Осипов, руководитель разработки iOS-приложений Облака Mail.Ru, преподаватель Технопарка Mail.Ru (20)
5. Бизнес-логика отправки файла в Облако
Можем добавить без «лагов»
6,7ms / 2,6 ms = 3
Создание
объекта
BLOB
Добаление
в очередь
задачи
Обновление
локального
кэша
Отправка
сетевых
запросов
2,6 ms
29. POSSchedulableObject: реализация компонент
Компонент Реализация
Event Блоки Objective-C
Event Queue Внутренняя реализация dispatch_queue_t из GCD
Run Loop Внутренняя реализация dispatch_queue_t из GCD
Thread Внутренняя реализация dispatch_queue_t из GCD
Scheduler RACTargetQueueScheduler из ReactiveCocoa
SchedulableObject Базовый класс для управляемых объектов
Паттерн SchedulableObject позволяет назначить объекту поток, в контексте которого должны осуществляться вызовы всех его методов. Согласованное назначение одного и того же потока нескольким объектам позволяет радикально упросить архитектуру приложения, превращая ее из многопоточной в 2-х или, в общем случае, N-поточную.
В данном разделе будет рассмотрен симптом высоконагруженной бизнес-логики, появление которого говорит о целесообразности использования паттерна SchedulableObject
Принято считать, что пользователь воспринимает интерфейс приложения плавным, если он в состоянии обновляться 60 раз в секунду. На эту цифру можно посмотреть в ином разрезе. Поделив 1000 мс на 60 фреймов мы получим, что в случае плавного интерфейса каждый фрейм должен успеть выполниться не более чем за 16,7 мс.
Допустим пользователь в определенный момент времени наблюдает некое окно, лайаут которого успевает отрисоваться за 10 мс. Это значит, что на бизнес-логика в рамках одного фрейма может потратить не более 6,7 мс.
Рассмотрим в качестве примера бизнес-логику добавления файла в Облако. Она состоит из множества этапов, суть которых в контексте данного доклада нас не интересует. Важно то, что все они вместе занимают 2,6 мс. Поделив максимальное время, отведенное на работу бизнес-логики, на время добавления одного файла, мы получим число 3. Оно означает, что если приложение хочет сохраняться отзывчивым при отработке данного кейса, оно не может позволить себе добавлять более 3 файлов единовременно.
С счастью для пользователя, но к сожалению для разработчиков, в приложении Облако существуют кейсы, когда необходимо добавить более 3 файлов за раз.
В данный момент, во избежание длительного «подвисания» приложения скорость добавления файлов в очередь на загрузку сервисом автозагрузки искусственно ограничивается методом «позорных констант». Две такие константы представленны на слайде. Их семантика такова: по итогам сканирования галереи на предмет новых фото, сервис должен добавлять их в очередь загрузки пачками не более 1000 штук каждая с интервалом 5 секунд. Но даже при таком ограничении мы имеем подвисание на 1000 * 2,6мс = 2,6 сек. каждые 5 сек., что не может не огорчать.
Искусственное ограничение пропускной способности бизнес-логики – признак ее высконагруженности. Паттерн SchedulableObject призван побороться с данным неприятным явлением.
Рассмотрим альтернативу методу «позорных констант». Для этого проследим эволюцию архитектуры классического бизнес-приложения. В рамках рассмотрения особый интерес представляют потоки (threads), в контексте которых происходит вызов методов объектов. Каждый из них мы будем кодировать своим собственным цветом. Изначально все происходит в главном потоке, поэтому все связи и сущности имеют одинаковый синий цвет.
Допустим, появился сценарий, при отработке которого один из объектов стал потреблять слишком много времени (более 6,7 мс). Из-за этого отзывчивость пользовательского интерфейса начинает страдать. Обозначим проблемный поток данных (data flow) жирными стрелками.
Без проведения рефакторинга ресурсоемкого класса осуществлять вызовы к нему в отельном красном потоке нельзя. Причина в том, что у него не один клиент, а два, и второй по-прежнему осуществляет вызовы из главного синего потока. Если эти вызовы изменяют разделяемое состояние объекта, то произойдет классическая проблема гонки к данным.
Для защиты от «гонки данных» необходимо реализовывать красный объект с предыдущего слайда потокобезопасным образом.
По мере развития проекта, еще один компонент становится узком местом. К счастью, у него только один клиент и потокобезопасная реализация не потребовалась.
Экстраполируя указанный подход к проектированию многопоточной архитектуры, рано или поздно она приходит к состоянию, изображенном на слайде.
При увеличении связей между объектами состояние архитектуры становится совсем плачевным. Недостатки данного подхода с условным названием Thread-Safe Architecture таковы:
Необходимость в постоянном отслеживании связей между объектами для своевременного проведения рефакторинга однопоточной реализации метода/класса на потокобезопасную (и обратно).
Потокобезопасные методы сложны в реализации, поскольку помимо прикладной логики необходимо учитывать специфику многопоточного программирования.
Активное использование примитивов синхронизации может в итоге сделать приложение еще более медленным, чем его однопоточная версия.
В мире серверной, десктопной и даже Android-разработки тяжелую бизнес-логику часто выделяют в отдельный процесс. Взаимодействие между сервисами внутри каждого из процессов продолжает носить однопоточных характер. Сервисы из разных процессов взаимодействуют друг с другом с использованием тех или иных механизмов межпроцессного взаимодействия (DCOM, Corba, .Net Remoting Boost.Interprocess и т.п.). К сожалению в мире iOS разработки мы ограничены лишь одним процессом и AS IS такая архитектура не подходит. Однако…
Мы можем адаптировать многопроцессную архитектуру на iOS-ный лад. Для этого мы заменяем процессы – потоками, а механизмы межпроцессного взаимодействия – косвенными вызовами. Выражаясь более формальным языком, суть трансформации такова:
Завести один отдельный рабочий поток.
Проассоциировать с ним цикл обработки событий и специальный объект для доставки в него сообщений – планировщик (от англ. scheduler).
Связать каждый изменяемый объект с одним из планировщиков. Чем больше объектов будет связано с планировщиками рабочих потоков, тем больше времени останется у главного потока на свою основную обязанность – обновление пользовательского интерфейса.
Выбирать правильный способ взаимодействия объектов друг с другом в зависимости от их принадлежности к планировщикам. Если планировщик общий, то взаимодействие осуществляется путем прямого вызова методов, если же нет, то опосредованно, через отправку событий.
Полученную архитектуру я и называю Schedulable Architecture.
В основе паттерна Schedulable Architecture лежат пять компонент. Далее для каждого из них определяется зона ответственности и предлагается наивная реализация с целью наиболее наглядным образом проиллюстрировать его внутреннее устройство.
Наиболее удобной абстракцией для событий в iOS являются блоки, внутри которых происходит вызов нужного метода объекта.
Поскольку события в очередь поступают из разных потоков, то очередь требует потокобезопасной реализации. Фактически именно она избавляет нас от трудностей многопоточной разработки в прикладных компонентах.
RunLoop реализует строго последовательную обработку событий из очереди. Именно благодаря этому свойству компонента паттерн SchedulableObject гарантирует, что все вызовы к реализующим его объектам осуществляются строго в одном потоке. В iOS SDK имеется стандартная реализация данного компонента – NSRunLoop.
Объект ядра операционной системы, в котором происходит исполнение кода цикла обработки сообщений. Наиболее низкоуровневой реализацией в iOS SDK является класс NSThread. Для практических целей рекомендуются к использованию более высокоуровневые примитивы вроде NSOperationQueue или очереди из Grand Central Dispatch.
Обеспечивает механизм доставки событий до требуемой очереди. Будучи главным компонентом, посредством которого клиентский код исполняет методы объектов, его он дает название как микропаттерну SchedulableObject, так и макропаттерну Schedulable Architecture.
Обеспечивает вызов методов объекта в строго определенном потоке. По отношению к целевому объекту может выступать как в роли агрегата, как в примере ниже, так и в роли базового класса, как в библиотеке POSSchedulableObject.
Продемонстрируем взаимодействие всех компонент архитектуры на примере консольного приложения. Его полный листинг доступен по ссылке – http://bit.ly/schedulable_object_concept. Приложение дублирует на консоли вводимые пользователем строки. Слой бизнес-логики, который мы хотим вынести из главного потока, представлен двумя классами:
Printer печатает подаваемые ему строки в консоль.
PrintOptionsProvider позволяет конфигурировать сервис Printer.
Assembly реализует паттерн Dependency Injection Container. Объекта данного класса создает сервисы бизнес-логики. Обратите внимание, что сервис PrintOptionsProvider инжектируется в сервис Printer AS IS, а снаружи оба сервиса доступны как SchedulableObject. Причина в том, что оба сервиса живую в одном и том же потоке и поэтому могут взаимодействовать друг с другом напрямую. Извне (в слое представления) они в видны под видом управляемых объектов, взаимодействие с которыми возможно только косвенным образом через отправку событий.
Слайд демонстрирует, как выглядит косвенное взаимодействие.
POSSchedulableObject – пример реализации паттерна на Objective-C. Библиотека доступна по ссылке – http://bit.ly/schedulable_object
В библиотеке реализован базовый класс, который:
Имеет ссылку на планировщик, через который должно происходить косвенное взаимодействие с объектом.
Автоматически проверяет корректность потока, из которого происходит вызов методов объекта-наследника. Достигается это за счет навешивания хуков (hooks) на все его методы в момент инициализации. В виду дороговизны данной процедуры по умолчанию она осуществляется только в отладочной версии приложения.
Основную часть исходников репозитория составляет демо-приложение. Оно авторизует пользователя в сервисе Dropbox, после чего выводит на экран имя и фамилию из его профиля. Рассмотрим несколько образцово-показательных примеров использования им библиотеки POSSchedulableObject.
Протокол POSSchedulable содержит методы для отправки событий реализующему его объекту. Их использование было продемонстрировано в примере выше. Класс POSSchedulableObject полностью реализует одноименный протокол. Кроме того, он добавляет проверки на предмет того, что методы объекта вызываются в правильном потоке. Из проверок исключаются свойства с атрибутом atomic. Для ручного исключения тех или иных методов существует специальный инициализатор с настройками исключений.
Пример объявления класса для управляемых объектов.
В консольном приложении, представленном в статье, коммуникация с объектом класса Printer была односторонней. Листинг на слайде показывает, как получить результат косвенного вызова и воспользоваться им в контексте потока вызвавшего его объекта.
Все объекты бизнес-логики приложения создаются внутри специальных классов, реализующих паттерн Dependency Injection Container. По аналогии с популярной библиотекой Typhoon, в названиях таких классов фигурирует корень Assembly. Создание объектов внутри них имеет две особенности:
Объекты создаются лениво, по запросу. Следствием этого является изменяющееся на протяжении времени жизни состояние объектов Assembly. Оно также защищается от многопоточного доступа путем наследования от POSSchedulableObject.
Возврат объектов попросившей их стороне происходит синхронно во избежании большого количества клиентского boilerplate-кода. Как видно из предыдущего листинга кода, использование accountInfoProvider достаточно многословно. Несложно представить, как приведенный код мог бы еще больше усложниться, если бы интерфейс Assembly имел асинхронную природу.
Таким образом, Assembly обязуется создать и вернуть любой сервис синхронно и только в главном потоке.
Все выглядит достаточно просто пока вдруг не потребуется создать граф объектов, которые , во-первых, живут в разных потоках, а во-вторых, для своей инициализации требуют вызвать один или несколько своих методов. Проблема в этом сценарии состоит в том, что для инициализации объекта A необходимо в красном потоке инициализировать объект B. Для того, чтобы с точки зрения клиента Assembly это произошло синхронно, на время создания красного объекта B синий поток должен быть заблокирован. Однако объекту B нужен объект C. Последний может создаться только в синем потоке. По аналогии с предыдущим шагом, на время его создания красный поток блокируется и ожидает завершения создания объекта C. Ожидание на этом этапе будет длиться вечно, поскольку событие, отправленное в синий поток, никогда не будет обработано, поскольку он был заблокирован при создании объекта A.
Выход из сложившейся ситуации заключается в том, чтобы блокировать поток с помощью специальной spin-блокировки. Она должна останавливать исполнение текущего потока, но при этом осуществлять прокручивание его цикла обработки сообщений. В рамках библиотеки POSSchedulableObject специально для этого случая предусмотрен метод posrx_await в категории к RACSignal.