суббота, 2 августа 2014 г.

[prog.sobjectizer] Подробнее о SObjectizer-5.4.0

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

Добавлен вспомогательный заголовочный файл so_5/all.hpp

Для тех, кому лень разбираться с тем, какие заголовочные файлы SObjectizer нужно подключать в своих исходниках, теперь добавлен один вспомогательный заголовочный файл so_5/api.hpp, который подтягивает в себя все публичные заголовки SObjectizer. Что позволяет писать вот так:

#include <so_5/all.hpp>

вместо, например, вот такого:

#include <so_5/rt/h/rt.hpp>
#include <so_5/api/h/api.hpp>
#include <so_5/disp/one_thread/h/pub.hpp>
#include <so_5/disp/active_group/h/pub.hpp>

Добавлен новый тип mbox-а: multi-producer/single-consumer

До версии 5.4.0 в SObjectizer были только multi-producer/multi-consumer (MPMC) mbox-ы. Т.е. любое количество получателей сообщений могло подписаться на mbox и любое количество отправителей могло отсылать сообщения в этот mbox. Методы create_local_mbox() и create_local_mbox(name) класса so_environment_t создают именно MPMC mbox-ы.

MPMC mbox-ы можно сравнить с досками объявлений, когда любой желающий может вывесить свое сообщение на доску и любой желающий сможет прочитать вывешенные сообщения. В SObjectizer MPMC mbox-ы используются для реализации publish-subscribe модели, где в качестве понятия "темы", на которую происходит подписка, выступает тип сообщения.

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

Для того, чтобы ситуации с mbox-ами с единственным получателем можно было обрабатывать более естественным и эффективным способом, в версии 5.4.0 был добавлен еще один тип mbox-а -- multi-producer/single-consumer (MPSC), называемый так же direct-mbox-ом. Такой mbox "намертво" связан с единственным агентом-получателем. И только этот агент может подписываться на сообщения из этого mbox-а. Все желающие могут отсылать сообщения в MPSC mbox, но только один агент сможет их получать.

MPSC mbox-ы создаются автоматически для каждого агента. Как только конструктор базового типа so_5::rt::agent_t завершает свою работу, у агента уже есть свой персональный, анонимный MPSC mbox. Доступ к которому можно получить посредством метода agent_t::so_direct_mbox().

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

Наличие MPMC и MPSC mbox-ов может поначалу запутать пользователя. Однако, это только на первый взгляд. MPMC mbox-ы, как и раньше, предназначены для работы в стиле publish-subscribe. Поэтому MPMC mbox-ы должны использоваться в случае, когда агент производит какую-то информацию и должен отдавать ее куда-то наружу, где кто-то может ее обработать. Например, агент может опрашивать датчик температуры воздуха и отсылать сообщения с очередным замером в MPMC mbox. А независимые получатели этой информации, коих может быть несколько, подписываются на сообщения этого MPMC mbox-а и обрабатывают сообщенную агентом информацию.

MPSC mbox-ы же предназначены для работы в стиле peer-to-peer. Классический пример -- ping-pong, когда два агента, каждый со своим MPSC mbox-ом, обмениваются сообщениями ping и pong, отсылая их напрямую в MPSC mbox своего партнера. Поэтому MPSC mbox-ы должны использоваться в случае, когда один конкретный агент должен получать некую персональную информацию снаружи. Например, сообщения о необходимости приостановить работу агента или изменить параметры его работы, логичнее отсылать в персональный MPSC mbox этого агента, а не в какой-то общий MPMC mbox.

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

Как показал опыт перехода к использованию MPSC mbox-ов в тестах и примерах SObjectizer, переход к работе с so_direct_mbox() очень простой. Если агент создавал у себя в конструкторе анонимный mbox и сохранял его в атрибуте с именем m_self_mbox, это практически всегда говорило о том, что такой mbox можно выбросить, а вместо него использовать so_direct_mbox().

На уровне реализации между MPMC и MPSC mbox-ами есть очень большая разница.

  • во-первых, в MPSC mbox-е нет стадии поиска списка получателей по типу сообщения. Есть только проверка того, что сообщение допустимо к обработке в текущем состоянии агента. Поэтому отсылка сообщений в MPSC mbox дает более высокую пропускную способность, чем в случае с MPMC mbox-ом;
  • во-вторых, как следствие из "во-первых", через MPSC mbox в очередь заявок агента можно поставить даже те сообщения, на которые агент не подписывался. Происходит это потому, что нет стадии поиска списка получателей по типу сообщения, любое сообщение просто напрямую ставится в очередь агента. И, если агент в нем незаинтересован, то сообщение будет отброшено когда до него дойдет очередь. С одной стороны, это дает возможность по ошибке "заспамить" агента, набросав ему в персональный MPSC mbox ненужных сообщений. С другой стороны, это позволяет получать интересный эффект. Например, агенту отправляются сообщения A, B, C и D в момент, когда агент подписан только на сообщение A. Тем не менее, в очереди окажутся все четыре сообщения. Во время обработки сообщения A агент подписывается на B и, когда очередь доходит до B, обрабатывает B. В процессе обработки он подписывается на C. И, соответственно, следом обрабатывает C. Если бы агент использовал вместо MPSC mbox-а MPMC mbox, то сообщения B, С и D к нему в очередь просто не встали бы, т.к. на момент их отсылки агент был подписан только на сообщение A.

Примечание. На данный момент использование MPSC mbox-ов возможно только при работе с обычными агентами. При определении ad-hoc агента нет способа получить доступ к его персональному MPSC mbox-у, создаваемому внутри SObjectizer-а. Т.е. MPSC mbox для ad-hoc агента внутри SObjectizer-а есть, но снаружи он недоступен.

Новый формат метода agent_coop_t::add_agent()

Метод agent_coop_t::add_agent() теперь возвращает указатель на агента, который был добавлен в кооперацию. Возвращенный указатель остается валидным в течении всего времени жизни кооперации.

Данный метод упрощает работу с direct-mbox-ами агентов. Например, вот так можно сделать обмен direct-mbox-ами в примере ping-pong:

class pinger_t : public so_5::rt::agent_t
   {
   public :
      ...
      void set_ponger_mbox( const so_5::rt::mbox_ref_t & mbox )
      {
         m_ponger_mbox = mbox;
      }
      ...
   private :
      so_5::rt::mbox_ref_t m_ponger_mbox;
   };

class ponger_t : public so_5::rt::agent_t
   {
   public :
      ...
      void set_pinger_mbox( const so_5::rt::mbox_ref_t & mbox )
      {
         m_pinger_mbox = mbox;
      }
      ...
   private :
      so_5::rt::mbox_ref_t m_pinger_mbox;
   };

...
so_5::rt::so_environment_t & env = ...;
auto coop = env.create_coop( "ping_pong" );

auto pinger = coop->add_agent( new pinger_t(env) );
auto ponger = coop->add_agent( new ponger_t(env) );

pinger->set_ponger_mbox( ponger->so_direct_mbox() );
ponger->set_pinger_mbox( pinger->so_direct_mbox() );

Новый вспомогательный метод агента so_make_state()

Для того, чтобы объявить состояние агента в предшествующих версиях SObjectizer нужно было сделать две вещи: объевить у агента атрибут типа so_5::rt::state_t, затем проинициализировать этот атрибут в конструкторе агента. Теперь в класс agent_t добавлен вспомогательный метод so_make_state(), который позволяет обойтись всего одним действием: объявлением и немедленной инициализацией атрибута:

class my_agent_t : public so_5::rt::agent_t
   {
   private :
      // Анонимное состояние.
      const so_5::rt::state_t st_first = so_make_state();
      // Именованное состояние.
      const so_5::rt::state_t st_second = so_make_state( "second" );
      ...
   };

При подписке на сообщение можно строить цепочку вызовов event()

Ранее цепочка so_subscribe(mbox).in(state).event(evt) могла содержать в себе несколько вызвовов метода in(). Это позволяло задать обработчик сообщения сразу для нескольких состояний. Но вызов метода event() должен был быть всего один. Т.е. каждый вызов so_subscribe() позволял задавать всего один обработчик сообщения.

Теперь вызовы event() так же можно объединять в цепочку. Что позволяет задать в одном so_subscribe() несколько обработчиков для разных сообщений. Например:

void so_define_agent() override
   {
      so_subscribe( so_direct_mbox ).in( st_disconnected )
         .event( so_5::signal< msg_reconnect >, &my_agent::evt_reconnect )
         .event( &my_agent::evt_send_when_disconnected )
         .event( so_5::signal< msg_get_status >,
               []() -> std::string { return "disconnected"; } );
      ...
   }

Примечание. Рекомендуется, чтобы все вызовы методов in() в цепочке so_subscribe()...event() были сделаны до первого вызова event(). Если in() и event() перемешенны, то реакция SObjectizer на эту последовательность может измениться в будущих версиях и пользователь может столкнуться с новым поведением SObjectizer, а его код перестанет работать. В версии же 5.4.0 вызов event() приводит к определению обработчика для всех состояний, которые были перечисленны в in() до вызова event(). Т.е. в цепочке so_subscribe(mbox).in(s1).in(s2).event(e1).in(s3).event(e2) событие e1 будет определено для состояний s1 и s2 (но не в s3!), тогда как событие e1 будет определено во всех трех состояниях (s1, s2, s3). Цепочка же so_subscribe(mbox).event(e3).in(s4) смысла вообще не имеет. Событие e3 будет определено для состояния по умолчанию, а упоминание состояния s4 будет проигнорированно.

Появился флаг thread_safety для обработчиков событий

По умолчанию SObjectizer считает, что обработчики событий не являются безопасными для одновременного их вызова на нескольких нитях (not_thread_safe-обработчики). И штатные диспетчеры SObjectizer обеспечивают пользователю гарантию того, что в конкретный момент времени у агента работает всего лишь один not_thread_safe обработчик и только на контексте одной единственной нити.

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

  • ни один not_thread_safe-обработчик не будет запущен до тех пор, пока работают какие-то другие обработчики агента;
  • not_thread_safe-обработчик не может работать одновременно с любым другим обработчиком;
  • ни один thread_safe-обработчик не будет запущен до тех пор, пока есть работающий not_thread_safe-обработчик.

Т.е, если у агента есть not_thread_safe-обработчики и thread_safe-обработчики, то сам SObjectizer будет заботится о том, чтобы запускать thread_safe обработчиков только в отсутствии not_thread_safe-обработчиков. А так же о том, чтобы not_thread_safe-обработчики запускались только после того, как закончат работать ранее запущенные thread_safe обработчики.

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

void so_define_agent() override
   {
      so_subscribe( so_direct_mbox ).in( st_disconnected )
         // Дополнительно ничего не указано.
         // Это not_thread_safe-обработчик и он не может
         // работать параллельно с другими обработчиками,
         // т.к. изменяет состояние агента.
         .event( so_5::signal< msg_reconnect >, &my_agent::evt_reconnect )
         // Это thread_safe-обработчик, т.к. он не изменяет состояние агента.
         .event(
               &my_agent::evt_send_when_disconnected,
               so_5::thread_safe )
         // Это thread_safe-обработчик, т.к. он не изменяет состояние агента.
         .event( so_5::signal< msg_get_status >,
               []() -> std::string { return "disconnected"; },
               so_5::thread_safe );
      ...
   }

Новый диспетчер thread_pool

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

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

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

Может использоваться механизм individual FIFO. Это означает, что агент, для которого указано использование такого механизма, будет рассматриваться независимо от других агентов его кооперации. И обработчики событий этого агента могут запускаться параллельно с обработчиками событий других агентов этой кооперации.

Т.к. disp_binder-ы при добавлении агентов в кооперацию могут быть заданы для каждого агента персонально, при работе со thread_pool-диспетчером можно сделать так, чтобы часть агентов кооперации использовала механизм cooperation FIFO, а другая часть -- механизм individual FIFO:

so_5::rt::so_environment_t & env = ...;
auto coop = env.create_coop( "channel_handler",
      // Привязка агентов к диспетчеру thread_pool с параметрами
      // по умолчанию (т.е. используется cooperation FIFO).
      so_5::disp::thread_pool::create_disp_binder( "thread_pool" ) );

// Этот агент использует binder кооперации, поэтому будет
// работать с механизмом cooperation FIFO.
coop->add_agent( new a_incoming_data_parser_t(env, ...) );
// Этот агент так же использует binder кооперации, поэтому он
// так же будет работать с механизмом cooperation FIFO.
coop->add_agent( new a_outgoing_data_packer_t(env, ...) );

// Для этого агента задается механизм individual FIFO,
// поэтому он будет работать независимо от других агентов кооперации.
using namespace so_5::disp::thread_pool;
coop->add_agent( new a_channel_handler_t(env, ...),
      create_disp_binder( "thread_pool",
            // Настройка параметров для привязки этого агента.
            []( params_t & p ) { p.fifo( fifo_t::individual ); } );

Еще один важный параметр, который можно задать для привязки агентов к thread_pool-диспетчеру -- это количество заявок агента, которые можно обработать подряд перед переключением на обслуживание заявок другого агента. Этот параметр задается посредством метода max_demands_at_once() класса so_5::disp::thread_pool::params_t. Если он отличен от единицы, то когда агенту выделяется рабочая нить, то рабочая нить пытается последовательно выполнить max_demands_at_once() заявок для этого агента не переключаясь на заявки других агентов. Только если заявки в очереди агента закончатся или же будет достигнуто количество max_demands_at_once(), произойдет переход к обслуживанию других агентов.

Примечание. Если часть агентов кооперации работает с механизмом cooperation FIFO, то параметр max_demands_at_once() распространяется на всех этих агентов, а не на каждого из них. Т.е., если в кооперации пять агентов и у каждого из них есть 10 заявок в очереди, то при параметре max_demands_at_once() равному 20, за один раз будет обработано лишь 20 заявок из всех этих 50, а не по 10 заявок для каждого агента.

Такая логика работы диспетчера thread_pool делает его удобным для использования в следующих случаях:

  • есть большое количество "мелких" агентов, сообщения для которых возникают эпизодически и группами. Для таких агентов удобными являются диспетчеры one_thread и active_group, но если агентов слишком много, чтобы работать на одной нити (например, несколько сотен тысяч), то проще привязать их к thread_pool-диспетчеру, который сможет обеспечивать рабочий контекст большему количеству "мелких" агентов без участия пользователя (тогда как при использовании one_thread-диспетчера пользователю пришлось бы создавать несколько таких диспетчеров и заботиться о распределении большого количества "мелких" агентов между этими one_thread-диспетчерами самостоятельно);
  • есть небольшое количество агентов, обработчики сообщений которых выполняют достаточно дительные операции. В этом случае thread_pool-диспетчер сможет разбрасывать "тяжелые" обработчики по свободным рабочим нитям, тем самым балансируя нагрузку между ними.

Новый диспетчер adv_thread_pool

Еще один новый штатный диспетчер, adv_thread_pool, очень похож на thread_pool-диспетчер. Диспетчер adv_thread_pool так же создает указанное пользователем количество нитей и распределяет работу привязанных к диспетчеру агентов между этими нитями. Но в работе adv_thread_pool-диспетчера есть два очень важных отличия от thread_pool-диспетчера:

  • во-первых, adv_thread_pool-диспетчер распознает флаг thread_safety для обработчиков событий агентов и может запустить параллельную обработку thread_safe-обработчиков одного агента сразу на нескольких нитях. При этом гарантируется, что работа not_thread_safe-обработчиков и thread_safe-обработчиков не будет пересекаться, а not_thread_safe-обработчики будут работать только по одному;
  • во-вторых, adv_thread_pool-диспетчер не позволяет задавать параметр max_demands_at_once при привязке агентов к диспетчеру. Обрабатывается всегда всего лишь одна заявка за один раз, после чего нить диспетчера может перейти к обслуживанию заявок других агентов.

Диспетчер adv_thread_pool, как и thread_pool-диспетчер, поддерживает механизмы cooperation FIFO и individual FIFO. При этом, если группа агентов кооперации требует механизма cooperation FIFO, то adv_thread_pool-диспетчер рассматривает все заявки этой группы агентов как заявки одного агента. И соответствующим образом обрабатывает флаг thread_safety для обработчиков событий агентов из этой группы.

Например, пусть в группу ходят агенты A1, A2 и A3. В очереди стоят заявки A1(M1), A2(M1), A1(M2), A3(M2), A2(M3). Обработчики A1(M1) и A2(M1) являются not_thread_safe. Поэтому они будут запущенны последовательно, один за другим. Обработчики A1(M2) и A3(M2) -- thread_safe, поэтому, при наличии двух свободных нитей диспетчера, эти обработчики будут запущенны параллельно друг другу. Обработчик A2(M3) -- not_thread_safe, поэтому он будет запущен только после завершения обработчиков A1(M2) и A3(M2). Какой из них завершится раньше, а какой позже заранее неизвестно, т.к. это будет зависеть как от самих обработчиков, так и от общей загруженности системы и особенностей диспетчеризации потоков в конкретной ОС.

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

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

Возможность добавлять диспетчеров после запуска SObjectizer Environment

До версии 5.4.0 все диспетчеры должны были быть заданы в объекте so_environment_params_t до запуска SObjectizer Environment. После того, как Environment заработает, добавить нового диспетчера было уже нельзя. Т.е., если вдруг обнаруживалось, что какой-то группе агентов нужен собственный экземпляр диспетчера active_group с именем "long_request_handler_disp", а он не был определен перед стартом Environment, то запустить этот экземпляр в динамике было невозможно.

В версии 5.4.0 в so_environment_t добавлен метод add_dispatcher_if_not_exists(), который позволяет добавить диспетчер к уже работающему SObjectizer Environment. Причем диспетчер будет добавлен только в случае, если в Environment еще нет диспетчера с таким же именем.

Использоваться этот метод может следующим образом:

void my_agent::evt_long_request( const request & evt )
{
   // Для обработки этого запроса нужна дочерняя кооперация,
   // которой потребуется свой собственный диспетчер.

   // Создаем диспетчер, если его еще нет.
   so_environment().add_dispatcher_if_not_exists(
         "long_request_handler_disp",
         []() { return so_5::disp::active_group::create_disp(); } );

   // Создаем дочернюю кооперацию, агенты которой будут
   // привязываться к собственному диспетчеру.
   auto coop = so_environment().create_coop(
         "long_request_handler",
         so_5::disp::active_group::create_disp_binder(
               "long_request_handler_disp",
               "child_coop" ) );
   coop->set_parent_coop_name( so_coop_name() );
   ...
}

Примечание. В версии 5.4.0 добавлена возможность в динамике добавлять новые диспетчеры, но нет возможности изымать ставшие ненужными диспетчеры. Т.е. добавленный посредством метода add_dispatcher_if_not_exists() диспетчер останется работать внутри SObjectizer Environment до полного завершения работы этого Environment-а. Объясняется это тем, что add_dispatcher_if_not_exists добавлен для нужд so_sysconf-а, в котором нужно иметь возможность расширить список диспетчеров уже после старта SObjectizer Environment, но нет необходимости затем уменьшать количество диспетчеров. Если на практике выяснится необходимость наличия средств изъятия ставших не нужными диспетчеров, то эта возможность будет добавлена в одной из следующих версий SObjectizer-а.

Режим autoshutdown в SObjectizer Environment

В предыдущих версиях SObjectizer для нормального завершения работы запущенного SObjectizer Environment-а нужно было где-то явным образом вызвать метод stop() класса so_environment_t. В больших приложениях, собранных из кубиков посредством so_sysconf, эта задача находилась в ведении so_sysconf-а. Но в небольших приложениях, где SObjectizer Environment запускается и останавливается "вручную", нужно решать, какой именно из прикладных агентов будет делать вызов stop() и как он может понять, что вся работа выполнена и вызов stop() ничего не оборвет.

В версии 5.4.0 был добавлен режим autoshutdown, при котором SObjectizer Environment автоматически завершает свою работу когда обнаруживает, что была дерегистрирована последняя работающая кооперация. Т.е. как только ни одной кооперации не остается, SObjectizer Environment автоматически останавливается.

Этот режим включен по умолчанию. Если же пользователю нужно, чтобы Environment продолжал работать и после дерегистрации всех коопераций, то при запуске SObjectizer Environment нужно вызвать метод disable_autoshutdown() у so_environment_params_t. Отключение режима autoshutdown может потребоваться, например, в случае GUI приложения, в котором создается кооперация для фонового выполнения какой-то задачи (например, сохранения большого документа на диск). В этом случае лучше один раз запустить SObjectizer Environment при старте приложения, а затем создавать кооперации при необходимости. Выполнив свою задачу кооперация дерегистрируется, но Environment продолжит работать и его не нужно будет перезапускать для создания новой кооперации.

Новые вспомогательные методы агента so_deregister_agent_coop() и so_deregister_agent_coop_normally()

Первый метод позволяет писать:

so_deregister_agent_coop( some_reason );

вместо:

so_environment().deregister_coop( so_coop_name(), some_reason );

Второй метод позволяет писать:

so_deregister_agent_coop_normally();

вместо:

so_environment().deregister_coop( so_coop_name(), so_5::rt::dereg_reason::normal );

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

Использование библиотеки ACE сокращено до минимума

Из библиотеки ACE сейчас в SObjectizer используется всего лишь:

  • средства работы с таймерами;
  • средства для логирования (макросы ACE_ERROR и ACE_DEBUG);
  • средства для работы с аргументами командной строки (только в тестах и примерах);

Полностью отказываться от ACE в версии 5.4.0 не стали из-за того, что ACE все равно потребуется в подпроектах (so_log, so_5_transport и so_sysconf). Однако, зависимость от ACE сейчас сильно уменьшена и, если со временем so_5_transport будет переведен на что-то другое (Boost.Asio или libuv, или libev), то от ACE в SObjectizer можно будет отказаться полностью.

Активное использование spinlock-ов для синхронизации доступа к разделяемым данным

Во многих случаях в версии 5.4.0 для синхронизации доступа к разделяемым данным стали использоваться spinlock-и вместо более тяжеловесных и медленных ACE_Thread_Mutex и ACE_RW_Thread_Mutex. В связи с этим удалено использование пулов мутексов и ряд методов, позволявших задавать таким объектам, как mbox-ы, свои собственные мутексы (AFAIK, эта возможность на практике не использовалась).

Отдельная благодарность Дмитрию Вьюкову за помощь и консультации в реализации spinlock-ов.

Имена у состояний агентов теперь есть всегда

Если пользователь не задает имя состояния самостоятельно, то оно все равно будет сгенерировано. Это сделано для упрощения отладки агентов и улучшения диагностики ошибок подписки агента или перевода его из одного состояния в другое.

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

Изменен способ организации очередей заявок для агентов

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

В версии 5.4.0 подход к организации очередей изменен. Локальная очередь заявок агента используется только для тех сообщений, которые агент успевает получить в интервал времени между вызовом so_subscribe() внутри so_define_agent() и окончательной привязкой агента к рабочей нити. В момент привязки сообщения из локальной очереди перемещаются в очередь рабочей нити. После чего заявки напрямую идут в очередь рабочей нити, а локальная очередь больше не используется. Такой механизм работы повышает пропускную способность штатных диспетчеров one_thread, active_obj и active_group. В двух новых штатных диспетчерах -- thread_pool и adv_thread_pool -- схема работы несколько сложнее, там есть очереди агентов и очередь нотификаций, но механизм доставки сообщений до агента там сложнее, чем в других штатных диспетчерах.

Новая процедура привязки агента к диспетчеру

Это изменение внутренней логики поведения SObjectizer Run-Time, которое вряд ли будет видно обычным пользователям SObjectizer-а. В версии 5.4.0 процедура привязки агентов к диспетчерам стала двухфазной.

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

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

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

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