Главным средством взаимодействия между агентами в SObjectizer являются сообщения. Но сами сообщения можно разделить на два фундаментальных подвида: собственно сообщения, которые переносят информацию, и сигналы, т.е. уведомления которые не несут больше никакой информации кроме самого факта своего существования. Например, пусть есть два агента, Master и Slave. Когда нужно что-то сделать, скажем, обработать порцию данных, Master отсылает Slave сообщение msg_process_data, в котором находятся подлежащие обработке данные. Когда работу нужно завершить, агент Master отсылает агенту Slave сигнал msg_shutdown. Этот сигнал никакой другой информации не переносит, важен сам факт возникновения этого сигнала.
В SObjectizer-4 первоначально был очень простой подход к сообщениям. Обработчик события должен был получать экземпляр сообщения по указателю, поэтому обработчик события у агента должен был иметь формат:
void a_slave_t::evt_process_data( const msg_process_data * msg ) { ... } void a_slave_t::evt_shutdown( const msg_shutdown * ) { ... } |
Если нужно было отослать сообщение, то динамически создавался его экземпляр, который отсылался агенту. Если же нужно было отослать сигнал, то выполнялись те же действия, что и при отсылке сообщения, но отсылался нулевой указатель на данные сообщения:
// Отсылка сообщения. msg_process_data * msg = new msg_process_data(...); so_4::api::send_msg( "a_slave", "msg_process_data", msg ); // Отсылка сигнала. so_4::api::send_msg( "a_slave", "msg_shutdown", 0 ); |
Однако, уже в самом начале использования SO4 стало понятно, что для сообщений, которые обязаны переносить данные, нужно обеспечивать null safety, т.е. на уровне SObjectizer-а запрещать попытки передачи нулевых указателей в качестве экземпляра такого сообщения.
Здесь нужно сказать пару слов о том, почему null safety для сообщений важен. Думаю, многие C++ программисты сталкивались с ситуацией, когда по ошибке передавали nullptr в std::string(const char *). Хотя все знают, что делать этого не следует, класс basic_string не содержит обработки нулевого указателя в своем конструкторе, но тем не менее, время от времени такое происходит. Приложение валится от access violation и потом приходится искать в отладчике причины краха. Аналогичные вещи иногда случаются и с агентами в SObjectizer, особенно когда подготовка объекта-сообщения записывается не одной-двумя строчками кода. Если подготовка экземпляра сообщения нетривиальна и задействует сторонний API, то запросто можно получить нулевой указатель, даже не заметив этого. Мне самому приходилось наступать на эти грабли и доводилось видеть последствия у коллег. Так что null safety окупается.
Первоначально null safety обеспечивалась слишком уж в духе Plain C: для сообщения можно было назначить т.н. checker -- функцию, которая проверяет экземпляр сообщения и говорит, корректен ли он или нет. Поскольку в рассматриваемом здесь примере обработчик msg_process_data не должен получать нулевых указателей на экземпляр сообщения, сообщение msg_process_data должно было бы быть снабжено checker-ом хотя бы следующего вида:
bool msg_process_data_checker( const msg_process_data * msg ) { // Не должно быть нулевым. return 0 != msg; } |
Вся эта затея с checker-ами в SO4 появилась из-за того, что в SO4 можно было практически любой тип использовать в качестве типа сообщения. И функция-checker могла проверять не только само наличие экземпляра сообщения, но и его инварианты (т.е. корректность значений внутри сообщения). Но на практике вышло, что в качестве типов сообщения задействовались только специально написанные для этих целей структуры данных. В которых корректность инвариантов гораздо проще обеспечивалась другими средствами, например, выполнением всех проверок непосредственно в конструкторе объекта-сообщения. Поэтому со временем выяснилось, что чуть ли не 99% всех checker-ов выполняют всего одно действие: проверку на равенство нулю указателя на экземпляр сообщения.
Естественно, заставлять разработчика писать для одной проверки отдельную функцию-checker, а потом еще и регистрировать ее для типа сообщения, было не правильно. Поэтому в SO4 был добавлен еще один тип формата обработчика событий:
void a_slave_t::evt_process_data( const msg_process_data & msg ) { ... } |
Т.е. если SO4 видел, что обработчик события получает объект сообщения по ссылке, то сам SO4 делал проверку наличия экземпляра сообщения. Что существенно упростило работу с сообщениями в SO4. Сигналы же в SO4, после введения таких проверок, продолжали обрабатываться как и раньше, через получающие указатели обработчики событий.
В SObjectizer-5 с сообщениями и обработчиками событий произошли серьезные изменения. Сообщения теперь должны быть наследниками специального базового типа so_5::rt::message_t, а обработчики событий получают не экземпляр сообщения непосредственно, а ссылку на специальный объект-обертку:
// Декларация сообщения. struct msg_process_data : public so_5::rt::message_t { ... }; // Обработчик события. void a_slave_t::evt_process_data( const so_5::rt::event_data_t< msg_process_data > & msg ) { ... } // Отсылка сообщения. std::unique_ptr< msg_process_data > msg( new msg_process_data(...) ); slave_mbox->deliver_message( std::move(msg) ); |
Обеспечению null safety для сообщений в SO5 так же было уделено внимание. Для этих целей предназначалась обертка not_null_event_data_t, которую нужно было указывать в качестве параметра обработчика события вместо event_data_t. Т.е. приведенный выше обработчик evt_process_data должен был бы быть описан так:
// Обработчик события. void a_slave_t::evt_process_data( const so_5::rt::not_null_event_data_t< msg_process_data > & msg ) { ... } |
Но тут я допустил два серьезных промаха. Во-первых, передача сообщений в SO-проектах происходит гораздо чаще, чем передача сигналов. Поэтому для описания обработчиков сообщений нужно было выбрать более удобное имя, а не not_null_event_data_t. Например, нужно было бы выбрать имена event_data_t и signal_data_t. Или же event_data_t и nullable_event_data_t. Мы же остановились на not_null_event_data_t и event_data_t, а я не заметил вовремя, к чему это приведет.
Во-вторых, на ранних стадиях использования SO5 я не проконтролировал корректное использование not_null_event_data_t и event_data_t в прикладом коде. Т.е. не навязал разработчикам дисциплины правильного выбора между not_null_event_data_t и event_data_t. Что привело к закономерному результату: event_data_t используется повсеместно, тогда как not_null_event_data_t можно было найти только в нескольких тестах и примерах самого SO5 (да и то в очень малых количествах).
В общем, когда дошли руки навести порядок с null safety для сообщений выяснилось, что уже есть большое количество кода с event_data_t, который просто так на not_null_event_data_t не перепишешь. Нужно было придумывать что-то другое. И вот что было сделано в разрабатываемой сейчас версии 5.2.
Во-первых, был выделен специальный класс so_5::rt::signal_t, наследник message_t. Этот класс должен стать базой для всех сигналов. Тогда как для сообщений продолжит использоваться message_t. Конструктор so_5::rt::signal_t сделал приватным, т.ч. наследника от signal_t объявить можно, а вот создать его экземпляр не получится. Так задумано специально, поскольку сигнал -- это уведомление без данных, какой-то объект создавать для него не нужно в принципе.
Во-вторых, тип not_null_event_data_t был удален вообще. Теперь все обработчики получают только ссылку на event_data_t. Вне зависимости о того, для сообщения предназначен обработчик или для сигнала. Лично мне это не очень нравится, но зато не нужно будет переделывать уже написанный на SO5 код.
В-третьих, в публичный API SO5 было добавлены специальные compile- и run-time проверки. Целью которых является: a) не дать возможности использования сигналов в качестве сообщений и b) не дать возможности отослать nullptr в качестве экземпляра сообщения. Например, посредством простой функции ensure_not_signal, использующей возможности C++11, класс event_data_t модифицирован так, что в compile-time пресекаются попытки получить доступ к внутренностям сигнала. Немного технических детали для иллюстрации:
template< class MSG > void ensure_not_signal() { static_assert( !std::is_base_of< signal_t, MSG >::value, "instance of signal_t cannot be used in place of instance of " "message_t" ); static_assert( std::is_base_of< message_t, MSG >::value, "message class should be derived from message_t" ); } template< class MSG > class event_data_t { public: //! Constructor. event_data_t( const MSG * message_instance ) : m_message_instance( message_instance ) {} //! Access to message. const MSG& operator * () const { ensure_not_signal< MSG >(); return *m_message_instance; } //! Access to raw message pointer. const MSG * get() const { ensure_not_signal< MSG >(); return m_message_instance; } //! Access to message via pointer. const MSG * operator -> () const { ensure_not_signal< MSG >(); return get(); } private: //! Message. const MSG * const m_message_instance; }; |
Т.е. если агент Slave в обработчике сигнала msg_shutdown попробует обратиться к event_data_t<msg_shutdown>::get() или event_data_t<msg_shutdown>::operator*(), то возникнет ошибка компиляции (если, конечно, разработчик не забудет унаследовать msg_shutdown от so_5::rt::signal_t).
Методы deliver_message так же получили дополнительные compile- и run-time проверки. Теперь они выглядят так:
template< class MSG > void ensure_message_with_actual_data( const MSG * m ) { ensure_not_signal< MSG >(); if( !m ) throw so_5::exception_t( "an attempt to send message via nullptr", so_5::rc_null_message_data ); } template< class MESSAGE > void mbox_t::deliver_message( std::unique_ptr< MESSAGE > && msg_unique_ptr ) { ensure_message_with_actual_data( msg_unique_ptr.get() ); deliver_message( type_wrapper_t( typeid( MESSAGE ) ), message_ref_t( msg_unique_ptr.release() ) ); } |
Т.е. теперь deliver_message во время компиляции проверяет, что пытаются оправить именно экземпляр message_t (т.ч. сейчас signal_t в deliver_message не засунешь). А в run-time проверяется, чтобы указатель на message_t был отличен от nullptr.
Для отсылки же сигнала в SObjectizer-5.2 один из вариантов deliver_message был переименован в deliver_signal, который в compile-time проверяет, чтобы в качестве сигнала не попробовали отослать message_t.
Вот как-то так получилось. Вроде бы выглядит логичнее и удобнее, чем в SObjectizer-5.1. Но как это пойдет на практике, нужно будет еще посмотреть. В любом случае, новые возможности C++11, в частности, type_traits, оказались очень к месту.
Комментариев нет:
Отправить комментарий