so_4: Версия 4.2.7. Insend-события

История вопроса

Изначально SObjectizer проектировался и разрабатывался как средство для поддержки исключительно асинхронного взаимодействия агентов через отсылку/получение сообщений. Но, как и любая "ортодоксальная" идея, чистая асинхронность в реальной жизни имеет стролько же недостатков, сколько и преимуществ.

Переодически пользователи SObjectizer-а высказывали пожелания в внедрении в SObjectizer понятия "синхронного" события. Но до версии 4.2.7 ничего подобного в SObjectizer-е реализовано не было, т.к. до сих пор сложно представить, каким образом в рамках одного продукта поддержки обмена сообщениями совместить асинхронное и синхронное взаимодействие.

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

Для того, чтобы агент-коммуникатор узнал о появлении глобального агента в so_4::api::make_global_agent() отсылается сообщение so_4::rt::msg_global_agent, на которое подписан агент-коммуникатор. Агент-коммуникатор обрабатывает это сообщение обычным образом. Именно здесь и возникли две проблемы:

Причины выбора в пользу insend-событий

Путей устанения проблемы с обработкой сообщения so_4::rt::msg_global_agent могло быть несколько.

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

Можно было бы реализовать понятие hook-ов для сообщений. Т.е. назначения процедур, которые должны были бы запускаться при попытке отослать конкретное сообщение. Причем запускались бы hook-и внутри so_4::api::send_msg() на контексте нити, отсылающей сообщение. Но такой способ можно было бы использовать как альтернативу стандартному способу диспетчеризации сообщений и событий. У разработчиков мог бы появиться соблазн при необходимости выполнения каких-либо синхронных действий создать hook для сообщения.

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

Так появилась идея insend-событий. Т.е. событий, для которых:

Insend-события

Insend-события отличаются от normal-событий только способом подписки. Для подписки insend-событий в класс so_4::rt::agent_t добавлены методы: so_4::rt::agent_t::so_subscribe_insend_event(). Так же для подписки insend-события может применяться API-функция so_4::api::subscribe_event(). Макросы SOL4_SUBSCR_EVENT_* для подписки insend-событий не применимы!.

Как и для обычного события, для insend-события можно указать произвольное количество инцидентов и любой приоритет. Если сообщение приводит к возникновению нескольких insend-событий разных агентов, то события запускаются на обработку в порядке убывания приоритетов. Порядок запуска на обработку insend-событий с одинаковым приоритетом не определен.

Insend-событие можно сделать обычным событием, если переподписать его с помощью so_4::rt::agent_t::so_subscribe() (API-функции so_4::api::subscribe_event() или макроса SOL4_SUBSCR_EVENT_START()). Аналогично, обычное событие можно сделать insend-событием, если переподписать его с помощью so_4::rt::agent_t::so_subscribe_insend_event().

Контекст

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

В случае же обычных событий необходимость синхронизации доступа к атрибутам агента зависит от используемого диспетчера. Существующие в версии 4.2.7 диспетчеры (с одной рабочей нитью, с активными агентами, с активными группами, диспетчеры главной нити для Windows и для Qt) гарантируют, что все normal-события агента последовательно запускаются на контексте одной и той же нити. Поэтому никакой синхронизации для normal-событий при использовании этих диспетчеров не нужно.

Отложенные и переодические инциденты

Внимание:
В случае, если инцидент для insend-события отсылается как отложенное или переодическое сообщение, insend-событие для этого экземпляра сообщения диспетчируется как normal-событие. Т.е. insend-событие не обрабатывается внутри вызова so_4::api::send_msg().
Объясняется это тем, что диспетчеризация отложенного или переодического сообщения осуществляется на контексте специальной таймерной нити. Как правило, это нить с высоким приоритетом и жесткими требованиями по времени реакции. Поэтому на ее контексте не желательно запускать какой-либо прикладной код.

Конфликты

Может сложится ситуация, когда два события одного агента подписаны на один и тот же инцидент. Если событие является normal-событием, а второе insend-событием, то при отсылки инцидента SObjectizer определит конфликт между normal- и insend-событиями.

Конфликт возникает из-за того, что на момент диспетчирования заявок на запуск обработчиков события SObjectizer не может определить, какую заявку нужно ставить в очередь, а какую обрабатывать в рамках send_msg. Дело в том, что два события одного агента от одного инцидента обрабатываются, только если они допустимы к обработке в разных состояниях агента. Проверка же состояния производится непосредственно перед запуском события на обработку, а не при постановке заявки в очередь диспетчера. Поэтому в момент обработки заявок для normal- и insend-событий SObjectizer не имеет права проверять состояние агента и на основании состояния делать вывод о том, как поступать с каждой из заявок.

Обнаружив конфликт, SObjectizer выдает на стандартный поток ошибок сообщение о конфликте, например:

so_4\rt\impl\event_data_only_one_of.cpp:154: Insend and normal events conflict detected!
	Agent: a_receiver_1
	Incident: a_receiver_1.msg_my
и диспетчирует оба события как normal-события.

Возможные применения

Insend-события не предназначены для того, чтобы привнести в SObjectizer чистую синхронность. Скорее insend-события дают возможность в некоторых случая выполнить какие-то действия сразу при их инициировании.

Самым ярким примером является уже описанный случай с обработкой сообщения so_4::rt::msg_global_agent. Благодоря insend-событиям агент-коммуникатор получает возможность подписаться на сообщения нового глобального агента еще до того, как регистратор глобального агента узнает, что регистрация завершилась успешно.

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

Предостережения

Insend-события не следует рассматривать как попытку внедрения в SObjectizer чистой синхронности. Скорее, insend-события, это еще один вид диспетчеризации. Более того, технически insend-события как раз и реализованы через специальную версию интерфейса so_4::rt::dispatcher_t. Только, в отличии от других диспетчеров (например, с активными объектами или активными группами), этот вспомогательный диспетчер создается и используется ядром SObjectizer-а автоматически и по мере необходимости.

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

Более того, вряд ли имеет смысл расчитывать на то, что insend-события будут и в следующих версиях запускаться на контектсе нити, на которой вызвана функция so_4::api::send_msg(). Это уже и сейчас не так: для отложенных и переодических сообщений insend-события запускаются так же, как и normal-события. Следует исходить только из того, что обработчик insend-события будет завершен до возврата из so_4::api::send_msg(). А как это будет сделано и на контексте какой нити будет запущен обработчик -- это уже задача SObjectizer-а и диспетчера.

Ниже рассматриваются два варианта неправильного использования insend-событий в попытке обеспечить синхронность.

Передача указателя на приемник результата в сообщении

Агент A отправляет агенту B сообщение в предположении, что агент B обработает это сообщение с помощью insend-события. Т.к. агент A желает получить результат обработки, он передает в сообщении указатель на буфер-приемник результата.

class A : public so_4::rt::agent_t
{
  public :
    ...
    // Сообщение, которое будет отослано агенту B.
    struct  msg_process
    {
      // Сюда агент B должен поместить результат.
      int * m_result;

      msg_process() : m_result( 0 ) {}
      msg_process( int * result ) : m_result( result ) {}

      static bool
      check( const msg_process * cmd )
      {
        return ( cmd && cmd->m_result );
      }
    };

    void
    evt_do_something()
    {
      // Отсылаем сообщение агенту B и ждем результата.
      int result;
      so_4::api::send_msg_safely( so_query_name(), "msg_process",
        new msg_process( &result ) );
      // Обрабатываем результат.
      if( result ) ...;
    }
};

class B : public so_4::rt::agent_t
{
  public :
    virtual void
    so_on_subscription()
    {
      so_subscribe_insend_event( "evt_process", "A", "msg_process" );
    }

    void
    evt_process( const A::msg_process & cmd )
    {
      // Выполняем какую-то обработку.
      ...
      // Сообщаем результат.
      *( cmd.m_result ) = r;
    }
};

Главная проблема здесь в том, что в агенте A подразумевается синхронная реакция агента B. Но явно в коде это нигде не отражено. Поэтому при сопровождении данного кода велика вероятность получения проблем в случае, если агент B будет переписан без использования insend-событий.

Ответное сообщение

Агент A отправляет агенту B сообщение в предположении, что агент B обработает это сообщение с помощью insend-события. Для сообщения результата обработки агент B отсылает агенту A ответное сообщение, которое агент A так же обрабатывает с помощью insend-события. В результате, при возврате из первого send_msg агент A уже будет знать, каким образом завершилась обработка в агенте B.

class A : public so_4::rt::agent_t
{
  private :
    // Результат обработки в агенте B.
    int m_result;

  public :
    ...
    // Сообщение, которое будет отослано агенту B.
    struct  msg_process
    {
      ...
    };

    // Ответное сообщение, которое отсылается A.
    struct  msg_process_result
    {
      ...
    };

    virtual void
    so_on_subscription()
    {
      so_subscribe_insend_event( "evt_process_result",
        "msg_process_result" );
    }

    void
    evt_do_something()
    {
      // Отсылаем сообщение агенту B и ждем результата.
      so_4::api::send_msg_safely( so_query_name(), "msg_process",
        new msg_process( &result ) );
      // Обрабатываем результат.
      if( m_result ) ...;
    }

    void
    evt_process_result(
      const msg_process_result & cmd )
    {
      m_result = cmd.m_result;
    }
};

class B : public so_4::rt::agent_t
{
  public :
    virtual void
    so_on_subscription()
    {
      so_subscribe_insend_event( "evt_process", "A", "msg_process" );
    }

    void
    evt_process( const A::msg_process & cmd )
    {
      // Выполняем какую-то обработку.
      ...
      // Сообщаем результат.
      so_4::api::send_msg_safely( "A", "msg_process_result",
        new A::msg_process_result( ... ) );
    }
};

Проблем здесь множество. Начиная от трудоемкости и неочевидности, и заканчивая тем, что агент A расчитан только на работу с одним агентом B. Например, что будет, если агент A вернется из send_msg и перед выполнением if( m_result ) его прервут, а в это время кто-то еще отошлет агенту A сообщение msg_process_result?

Еще несколько слов о синхронности

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

Поэтому попытки реализовать средствами SObjectizer-а какое-либо синхронное взаимодействие сейчас обречены на множество проблем. Как правило это:

Поэтому в завершении описания insend-событий хочется еще раз сформулировать основной вывод попыток внедрения синхронности в SObjectizer:


Документация по SObjectizer v.4.4 'Тебуломста'. Последние изменения: Thu Sep 18 10:26:48 2008. Создано системой  doxygen1.5.6 Intervale SourceForge.net Logo