пятница, 14 августа 2015 г.

[prog.c++] Забацал новый пример для SO-5.5.8, теперь доволен как слон :)

Впервые за фиг знает сколько времени сделал пример, отсылающий к старым-добрым временам, когда имел какое-то отношение к задачам АСУТП. Пример простенький и к реальности имеющий далекое отношение, но вспомнить молодость приятно.

Пример имитирует управление примитивными станками (machine), в которых есть двигатель (engine) и система охлаждения (cooler). Работающий двигатель нагревается. Когда нагревается выше 70 градусов, то включается система охлаждения. Если после этого двигатель продолжает нагреваться и его температура достигает 95 градусов, то двигатель отключается. Когда двигатель охлаждается до 50 градусов, он включается вновь. Система охлаждения отключается если двигатель охлаждается до 50 градусов (при этом двигатель может быть как включенным, так и выключенным).

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

По объему этот пример оказался чуть ли не самым большим, больше 600 строк кода. При этом задействованы практически все фичи SObjectizer-а: начиная от старых, базовых возможностей SO-5, заканчивая практически всеми новыми дополнениями, которые были реализованы за последние 1.5-2 года, включая lambda-функции в качестве событий, ad-hoc-агенты, приватные диспетчеры, фильтры доставки и самый новый диспетчер prio_one_thread::strictly_ordered. Даже агенты, которые реализуются в виде шаблонных классов, присутствуют. Нет только синхронного взаимодействия агентов :)

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

Возможно, я не могу быть объективным, но уже при написании кода даже относительно небольших примеров, лучше осознаешь, насколько же все-таки важны те вещи, которые мы в SObjectizer закладывали. И которые отличают SO-5 от аналогов вроде C++ Actor Framework или Just Thread Pro.

Ну, например, оформление агентов в виде отдельных C++ классов. На задаче вроде ping-pong -- это жуткий оверхед. Но уже при такой несложной имитации примитивного станка оказывается, что описывать агентов в виде классов проще. Тут отлично сочетаются ООП и разработка сверху вниз. В какой-то момент осознаешь, что тебе нужны агенты вот такого типа -- появляется класс, у которого есть имя, но нет пока содержимого. Потом понимаешь, какие данные ему нужны будут, какие данные он должен получать из внешнего мира, а какие сможет генерировать/накапливать сам -- появляется список атрибутов класса и конструктор. Потом понимаешь, в каких состояниях сможет находиться агент, какой набор сообщений он сможет обрабатывать, к каким событиям они будут приводить. Тут появляется возможность описать поведение агента и сделать методы-заглушки для его событий. И т.д. и т.п.

По ходу дела объем класса-агента растет. Но "структура" этого класса позволяет проще ориентироваться в его потрохах. Нужно узнать, какие сообщения приводят к каким событиям? Нет проблем, это описывается в so_define_agent(). Причем практически декларативно:

virtual void so_define_agent() override
{
   this >>= st_engine_off;

   st_engine_on
      .event< turn_engine_off >( &a_machine_t::evt_turn_engine_off )
      .event< turn_cooler_on >( &a_machine_t::evt_turn_cooler_on )
      .event< turn_cooler_off >( &a_machine_t::evt_turn_cooler_off )
      .event< update_status >( &a_machine_t::evt_update_status_when_engine_on );
   st_engine_off
      .event< turn_engine_on >( &a_machine_t::evt_turn_engine_on )
      .event< turn_cooler_on >( &a_machine_t::evt_turn_cooler_on )
      .event< turn_cooler_off >( &a_machine_t::evt_turn_cooler_off )
      .event< update_status >( &a_machine_t::evt_update_status_when_engine_off );
}

Нужно предпринять какие-то действия сразу после старта: опять нет проблем, есть специально предназначенный для этого so_evt_start():

virtual void so_evt_start() override
{
   // Periodic update_status signal must be initiated.
   m_update_status_timer = so_5::send_periodic_to_agent< update_status >(
         *this,
         std::chrono::milliseconds(0),
         std::chrono::milliseconds(200) );
}

Можно смотреть на это как на разные несущественные мелочи. Но когда объем кода агента начинает превышать сотню-другую-третью-... строк кода, то такие мелочи начинают играть. Ну а если кто-то хочет сказать, что класс на 300+ строк кода -- это есть плохой стиль и такие классы нужно дробить, то тут остается только позавидовать людям, которые не сталкивались с некоторыми предметными областями. Вроде того же взаимодействия с оборудованием. В общем, бывают случаи, когда агенты оказываются очень толстыми. И в SObjectizer с этим проблем нет.

Ну а если нам везет и агента можно представить двумя-тремя строчками кода, то и в этом плане за последний год SObjectizer сделал серьезный шаг на встречу пользователем: ad-hoc агенты позволяют обойтись минимумом усилий. Вот, например, оставаясь в рамках C++11 (в C++14 было бы еще проще):

void create_starter_agent(
   so_5::rt::agent_coop_t & coop,
   const machine_dictionary_t & dict )
{
   // A very simple ad-hoc agent will be used as starter.
   // It will work on the default dispatcher.
   coop.define_agent().on_start( [&dict] {
         dict.for_each(
            []( const std::string &, const so_5::rt::mbox_t & mbox ) {
               so_5::send< turn_engine_on >( mbox );
            } );
      } );
}

В общем, усилия, которые мы прикладываем, для того, чтобы на SObjectizer писать было проще и писать можно было меньше, потихоньку дают свои плоды. Правда, от присущей C++ многословности избавиться не получится. Например, очень много места занимают конструкторы, вроде вот таких:

a_statuses_analyzer_t(
   context_t ctx,
   so_5::rt::mbox_t status_distrib_mbox,
   float safe_temperature,
   float warn_temperature,
   float high_temperature)
   :  so_5::rt::agent_t{ ctx }
   ,  m_status_distrib_mbox{ std::move( status_distrib_mbox ) }
   ,  m_safe_temperature{ safe_temperature }
   ,  m_warn_temperature{ warn_temperature }
   ,  m_high_temperature{ high_temperature }
{}

Но тут уже придется ждать изменений в самом языке :)

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

Правда, SO-5 не предназначен для управления оборудованием в режиме жесткого реального времени, слишком сильно внутренности SO-5 завязаны на работу с динамической памятью. Но таких целей при его разработке и не преследовалось. Зато для имитационных задач или же для управления программными комплексами -- вполне, на мой взгляд. Ведь как раз в задачах, так или иначе напоминающих ИИС и АСУТП, агентный подход показывает себя весьма неплохо.

Ну а работа над 5.5.8 будет продолжена. На очереди еще два диспетчера (prio_one_thread::quoted_round_robin и prio_dedicated_threads::one_per_prio), а так же новые примеры их использования. Вряд ли успею сделать релиз 5.5.8 в августе, скорее дело идет к району 10-15 сентября. Но посмотрим, как пойдет.

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