среда, 17 июня 2015 г.

[prog.c++11.sobjectizer] Нужно ли сохранять метод so_define_agent()?

Если среди читателей есть пользователи SObjectizer-а или те, кто смотрел на SObjectizer, но по каким-то причинам не стал им пользоваться, то прошу помочь с поиском ответа на сабжевый вопрос.

У агентов есть метод so_define_agent() который предназначен для того, чтобы агент в момент своей регистрации в SObjectizer Run-Time мог выполнить подписки на нужные ему сообщения, перейти в нужное состояние и т.д.

Этот метод появился в самой первой версии SO-5.0 и сохраняет свою важную роль вплоть до SO-5.5. Однако, т.к. в следующей версии, 5.6.0, планируется нарушение совместимости, то можно пересмотреть и значимость so_define_agent(). Тем более, что существует мнение, что без so_define_agent() вполне можно обойтись и делать все настройки агента в конструкторах.

Посему у меня вопрос к читателям: а как вам было бы удобнее, с методом so_define_agent() или без него?

Особенно интересно мнение тех, кто посмотрел на SO-5, подумал, что все как-то через чур сложно и запутано, и отказался от дальнейшего знакомства с SO-5.


Я же пока попытаюсь объяснить, почему so_define_agent() представлялся важным. Хотя даже сейчас его можно не использовать, а делать все подписки в конструкторах своих агентов. Просто это не рекомендуется делать, поэтому ничего подобного не увидишь в штатных примерах/тестах.

Лично мое мнение такое: удобно разделять действия, относящиеся к конструированию C++ объекта, и действия, относящиеся к конструированию SObjectizer-агента. Для C++ объекта нужен конструктор, в котором определяются все начальные инварианты C++ объекта. Когда C++ объект становится агентом, вызывается специальный метод, в котором агент определяет себя в терминах SObjectizer-а (состояние, подписки). Т.е. в so_define_agent() агент оперирует вещами, которых нет для обычных C++ объектов и которые не нужны до тех пор, пока агент из обычного C++ объекта не превратился в полноценного агента. Причем все эти действия собраны в одном месте. Что хорошо, особенно когда знакомишься с кодом чужого агента: вся относящаяся к SObjectizer инициализация находится в so_define_agent(), посмотрел туда и разобрался что к чему.

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

Возможно, первая причина -- это недостаточная поддержка стандарта C++0x в то время, когда разработка SObjectizer-5 стартовала (2010-й год). Например, если не ошибаюсь, не все компиляторы поддерживали delegating constructors. А это означало, что если агент имел несколько конструкторов, то ему все равно нужно было выносить всю подписку в какой-то свой отдельный метод и вызывать этот метод из всех своих конструкторов. Представлялось более простым сделать виртуальный метод so_define_agent(), который сам SObjectizer вызовет, когда это будет нужно.

На данный момент эта причина уже не актуальна, т.к. delegating constructors сейчас поддерживают все мейнстримовые компиляторы.

Вторая причина -- это наличие коопераций. Кооперация -- это, с одной стороны, способ одновременно зарегистрировать группу связанных друг с другом объектов. С другой стороны, кооперация кооперация играет такую же роль, как и супервизор в Erlang-е с политикой all-for-one (т.е. если один агент "падает", то прекращают свою работу и все остальные агенты кооперации).

Есть несколько существенных отличий работы агентов в SO-5 от работы акторов в CAF и процессов в Erlang:

  • агенты в SO-5 используют механизм publish-subscribe. Т.е. если агент не подписался на сообщение, то сообщение вообще не попадет к агенту в очередь. Тогда как в CAF и в Erlang сообщение упадет в очередь актора/процесса в любом случае. А уже захочет актор/процесс его обрабатывать -- это уже другой вопрос. Например, в Erlang-е с механизмом selective receive сообщение может попасть на обработку не сразу, а когда процесс будет заинтересован в нем;
  • акторы/процессы в CAF/Erlang создаются и запускаются по одному. Тогда как в SO-5 группа агентов регистрируется единовременно, в рамках одной транзакции.

На практике это выливается вот во что. Допустим, нам нужно зарегистрировать группу агентов A, B и C. Каждый из них в начале работы отсылает какое-то сообщение остальным агентам (т.е. агент A отсылает сообщение B и C, агент B -- агентам A и C, С -- агентам A и B). Каждый агент будет работать на своем рабочем контексте. Из-за чего нельзя гарантировать какого-то строгого порядка старта работы этих агентов. Т.е. нельзя дать гарантий того, что агент A стартует до агента B, а агент B -- до агента C.

Собственно, возникает вопрос, как агентам подписаться на сообщения друг друга еще до того, как они начнут работать? Ведь если этого не сделать и агенты будут подписываться при старте, то может случиться так, что агент B стартует раньше всех, отошлет свое сообщение агентам A и C, но сообщение никуда не уйдет, т.к. A и C еще не стартовали.

Метод so_define_agent() является ответом на этот вопрос. SObjectizer гарантирует, что so_define_agent() будет вызван для всех агентов кооперации еще до того, как агенты начнут работать. Т.е. в своих so_define_agent() агенты A, B и C спокойно подписываются на сообщения друг друга. И только после этого начинают работать. Поэтому не важно, стартовал ли агент B раньше агента A или нет. Даже если B стартовал раньше A, то отправленные B сообщения все равно окажутся в очередях агентов A и C.

На данный момент эта причина так же не кажется актуальной, т.к. агенты A, B и C могут делать подписки в конструкторах, еще до того, как они будут переданы в SObjectizer.

Третья причина -- это наличие наследования для агентов. Т.е. написанный Васей Пупкиным агент A может использоваться Федей Сидоровым в качестве базы для его собственного агента X: Федя просто наследует X от агента A. Но возникает вопрос: как Федя может поменять поведение Васиного агента? Например, полностью заменить подписки, сделанные агентом A?

Сейчас, если Вася выполнял подписки только в so_define_agent(), это сделать не сложно. Федя просто переопределяет so_define_agent() в классе X и не вызывает там A::so_define_agent(). А вот если подписки в классе A выполняются везде, где Вася счет это уместным, т.е. в конструкторах и методах-сеттерах, то у Феди головной боли будет побольше. Ему нужно будет отменять все подписки вручную.

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

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


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

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