суббота, 30 апреля 2016 г.

[prog.actors] Интересная задачка на применимость модели акторов и акторных фреймворков

В комментариях к одной из предыдущих заметок ув.тов.Dmitry Popov (aka thedeemon) подкинул интересную задачку.

Суть такая. Интерактивное приложение, где пользователь тыкает в произвольную позицию на таймлайне. Надо показать соответствующий кадр из видео, но кадр этот должен пройти обработку цепочкой фильтров. Причем любой фильтр для получения кадра N может захотеть иметь кадры N-2, N-1, N и N+1, поэтому чтобы показать пользователю кадр 123, прошедший через три фильтра, надо чтобы фильтр_1 получил на вход и обработал кадры 120-125, фильтр_2 из них сделал 121-124, а фильтр_3 сделал бы из них кадр-результат 123. Дальше пользователь может захотеть следующий кадр, 124, для которого фильтру_3 понадобятся кадры 122-125 от фильтра_2 (часть которых он уже сделал, но не все), тому - кадры 121-126 от фильтра_1 и т.д. А может пользователь захотеть кадр 200, и все эти 120-126 уже не нужны. Фильтры небыстрые, надо бы им работать в разных потоках параллельно. Цепочка фильтров не статична, пользователь может ее менять, а также временно отключать некоторые фильтры (как в фотошопе слой невидимым сделать).

Логика работы при этом получается следующая:

  • gui говорит фильтру_3: дай мне кадр 123;
  • фильтр_3 понимает, что ему нужны кадры 121-124 и запрашивает их у фильтра_2;
  • фильтр_2 понимает, что ему нужны кадры 120-125 и запрашивает их у фильра_1;
  • ...
  • пока все задействованные в цепочке обработки фильтры занимаются своим делом, от gui может прилететь команда "дай мне кадр 200";
  • фильтр_3 в этом случае говорит фильтру_2 "забей на кадры 121-124 и займись кадрами 198-201";
  • фильтр_2 в этом случае говорит фильтру_1 "забей на кадры 120-125 и займись кадрами 197-202"...

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

Собственно, задачка в том, чтобы понять, как эту задачу можно решать с помощью акторов вообще. Ну и применительно к фреймворкам, вроде SObjectizer и CAF в частности.

Что ж, попробуем пофантазировать.

Думается, тут удобно было бы использовать очереди заявок. Т.е. у каждого фильтра свой рабочий контекст и своя очередь заявок. Так, заявка на кадр 123 ставится фильтру_3. Тот ставит заявку на кадры 121-124 фильтру_2, тот -- заявку на кадры 120-125 фильтру_1. Когда фильтр_1 завершает обработку кадров 120-125, он выставляет результат в очередь заявок фильтра_2. Тот, после обработки кадров 121-124 -- в очередь заявок фильтра_1.

Главная сложность здесь в том, чтобы уметь отзывать заявки, которые уже не актуальны.

Как вариант, для первоначального запроса можно создать atomic-переменную со статусом. Ссылка на эту переменную передается во всех запросах: когда фильтр_3 отсылает запрос фильтру_2, когда фильтр_2 отсылает запрос фильтру_1 и т.д. Когда получатель запроса извлекает запрос из очереди, он проверяет статус. Если запрос еще актуален, он выполняется. Если не актуален, то его результат выбрасывается.

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

  • каждый фильтр представляется агентом, привязанным к какому-то контексту (будь то отдельная рабочая нить для каждого агента или же пул потоков для всех агентов-фильтров);
  • GUI создает запрос с atomic-статусом внутри запроса и отсылает запрос агенту "фильтр_3";
  • агент "фильтр_3" отсылает свой запрос агенту "фильтр_2". В этом новом запросе идет ссылка на atomic-статус из оригинального запроса и почтовый ящик агента "фильтр_3" для ответа (почтовый ящик -- это особенность SObjectizer, в CAF это была бы ссылка на актор, в Erlang-е -- pid процесса);
  • агент "фильтр_2" аналогичным образом отсылает свой запрос агенту "фильтр_1";
  • агент "фильтр_1" выполняет свои действия и отсылает ответ агенту "фильтр_2";
  • агент "фильтр_2" получает ответ от "фильтр_1", выполняет свои действия и отсылает свой ответ агенту "фильтр_3";
  • агент "фильтр_3" получает ответ от "фильтр_2", выполняет свои действия и отсылает ответ в GUI.

При этом каждый агент перед обработкой запроса проверяет статус. Если запрос не актуален, то никакая работа над ним не производится. Соответственно, в GUI, если пользователь не захотел дожидаться кадра 123 и выбрал кадр 200, достаточно сбросить статус в запросе для кадра 123.

Эту схему можно сделать хитрее, если каждый агент-фильтр будет представлен двумя агентами: анализатором и исполнителем. Все анализаторы должны работать очень быстро и их все можно разместить на одном рабочем контексте. Задача анализатора -- получить запрос, понять, чего не хватает для его выполнения и поставить заявку соответствующему агенту. Так, от GUI запрос для кадра 123 сначала идет агенту-анализатору "фильтр_3". Тот понимает, что нужны кадры 121-124 и делает запрос агенту-анализатору "фильтр_2". Тот понимает, что нужны кадры 120-125 и делает запрос агенту-анализатору "фильтр_1". Тот дает команду агенту-исполнителю "фильтр_1". Результат работы адресуется агенту-исполнителю "фильтр_2". Результат -- агенту-исполнителю "фильтр_3" и т.д.


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

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


Не знаю, каким именно образом фильтры будут получать исходные кадры. Может быть "фильтр_1" может сразу взять кадры 120-125, а затем одним махом выдать их все уже обработанными. Может быть "фильтр_1" должен получать кадры по одному и отдавать потом будет так же по-одному.

Если с кадрами нужно работать по-одному, то для выполнения запросов от GUI может потребоваться более хитрая схема работы. Например, следующая:

Для обработки кадра 123 GUI создает агент "контроллер-123", этот агент выдает запрос агенту "фильтр_3". Агент-фильтр понимает, что ему нужны кадры от агента "фильтр_2". И для получения этих кадров создается агент "контроллер-123--121-124", который высылает запрос агенту "фильтр_2". Тот создает агента "контроллер-123--121-124--120-125". Этот контроллер выдает запрос агенту "фильтр_1".

Когда агент "фильтр_1" обрабатывает кадры 120-125, он отсылает их агенту "контроллер-123--121-124--120-125". А этот контроллер пересылает кадры результат агенту "фильтр_2". Когда агент "контроллер-123--121-124--120-125" перешлет все кадры, он закончит свою работу.

Агент "фильтр_2" выполняет обработку поступающих к нему кадров и отсылает результаты агенту "контроллер-123--121-124". Тот выдает результаты агенту "фильтр_3". Когда все результаты пересланы он завершает свою работу. Результат от "фильтр_3" поступает агенту "контроллер-123", а оттуда уже к GUI.

Все агенты-контроллеры делаются дочерними друг другу. Т.е. "контроллер-123--121-124--120-125" является дочерним агенту "контроллер-123--121-124", а тот -- агенту "контроллер-123". Соответственно, если дерегистрируется "контроллер-123", то автоматически дерегистрируются и все его дочерние агенты.

Поэтому, если в GUI пользователь вместо кадра 123 выбрал кадр 200, то агент "контроллер-123" дерегистрируется и это приводит к дерегистрации всех его дочерних агентов. Тем самым прерывается цепочка обработки кадра 123.

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


Вероятно, если чуток погрузиться в детали, можно будет придумать и другие схемы работы. Или же тем или иным образом модифицировать описанные выше схемы. Меня лично порадовало другое: представляется, что в SObjectizer есть все необходимые инструменты для реализации описанных выше схем. Ну и механизм диспетчеров SObjectizer будет здесь очень в тему, особенно способность SObjectizer-а привязывать агентов одной кооперации к разным диспетчерам.

Комментариев нет: