- PVSM.RU - https://www.pvsm.ru -
Когда мы начали рассказывать про свой OpenSource акторный фреймворк для C++ на Хабре, мы пообещали описывать некоторые особенности деталей реализации SObjectizer-а. Одна из новых фич, которая была реализована в недавно вышедшей версии 5.5.19 [1], отлично подходит для такого рассказа. Кроме того, она интересна еще и тем, что нам пришлось взглянуть на сценарии использования SObjectizer с совершенно другой стороны. Можно даже сказать, что один из наших шаблонов оказался разорванным.
Речь идет о возможности SObjectizer-а выполнять все свои действия на одной единственной рабочей нити. Начиная с версии 5.5.19 использовать Actor- и Publish/Subscribe модели можно даже в однопоточном приложении. Понятное дело, что акторы должны будут работать в режиме кооперативной многозадачности, но в каких-то случаях именно это и требуется.
Как оказалось, есть целый класс задач, где нужны маленькие легковесные приложения. Внутри которых использование акторов вообще и SObjectizer-а в частности уместно, а вот создание нескольких рабочих нитей и связанные с этим накладные расходы — это уже как из пушки по воробьям.
Скажем, у нас может быть большое приложение, состоящее из основного master-процесса и дочерних процессов-worker-ов, коих может быть хоть сто, хоть тысяча. Master-процесс распределяет работу по worker-ам и забирает результаты их работы, а также контролирует жизнеспособность worker-ов, перезапуская их по мере надобности. Дочерние worker-ы, как правило, должны быть простыми и легковесными процессами. Очень хочется, чтобы каждый из них обходился всего одной рабочей нитью. Ведь одно дело иметь в системе тысячу процессов-worker-ов с одной нитью внутри, совсем другое — тысячу worker-ов с четырьмя рабочими потоками внутри.
Или другой пример: маленькая программка, которая должна время от времени опрашивать пару устройств и отсылать снятые данные MQTT-шному брокеру. Работа с каждым из устройств может быть оформлена в виде агентов. Но многопоточность здесь вряд ли потребуется. Тем более, что работать все это может на небольшом одноплатнике с ограниченными ресурсами и даже если сам одноплатник вполне тянет обычный Linux-овый дистрибутив, то все равно нет смысла расходовать ресурсы без должных на то оснований.
Изначально SObjectizer создавался как инструмент для упрощения разработки больших и сложных многопоточных приложений. SObjectizer-овские диспетчеры и взаимодействие агентов только посредством асинхронных сообщений позволяют писать приложения с десятками, а то и сотнями, рабочих потоков внутри, при этом программисту не приходится иметь дело ни с одним mutex-ом или condition_variable. Поэтому сегмент небольших однопоточных приложений мы даже не рассматривали в качестве ниши для применения SObjectizer-а. Как оказалось, зря. Модели Акторов и Publish/Subscribe вполне хорошо себя чувствуют и в однопоточных приложениях.
Сперва нужно рассказать, зачем SObjectizer-у вообще было нужно несколько рабочих потоков. Эти рабочие потоки нужны для:
Получается, что когда обычный SObjectizer запускается посредством вызова so_5::launch, то текущая нить (т.е. та на которой был вызван so_5::launch) используется для выполнения начальных действий, после чего блокируется до момента завершения работы SObjectizer Environment. Попутно SObjectizer создает три описанных выше нити для таймера, окончательной дерегистрации коопераций и дефолтного диспетчера. Плюс столько нитей, сколько потребуется дополнительным, созданным пользователем, диспетчерам.
Мы захотели чтобы SObjectizer мог делать все нужные ему операции на контексте всего одной нити — той, на которой и произошел вызов so_5::launch.
Для этого нам потребовалось ввести новое понятие — environment infrastructure, т.е. инфраструктура, которая будет обслуживать нужды самого SObjectizer-а. Был сделан соответствующий интерфейс, переделаны внутренности SObjectizer Environment, чтобы в нужных местах дергались методы этого интерфейса. Ну и затем было сделано несколько реализаций:
В основе простых однопоточных инфраструктур лежит единственный цикл, внутри которого SObjectizer Environment последовательно выполняет следующие действия:
При этом, очевидно, точность работы таймера начинает зависеть от того, какие агенты работают на дефолтном диспетчере: если эти агенты быстро обрабатывают свои события, то таймер работает более-менее точно. Если же обработка может затягиваться на секунды или на десятки секунд, то точность таймера оказывается никакой и после завершения длительного обработчика может быть сгенерирована сразу пачка таймерных событий. Но это вполне естественная плата за отсутствие отдельной таймерной нити.
Слово «simple» в названиях simple_mtsafe и simple_not_mtsafe используется не просто так, а потому, что дефолтный диспетчер применяет простую FIFO схему обработки событий без учета приоритетов агентов. Если кому-то нужна однопоточная инфраструктура с поддержкой приоритетов агентов, то дайте знать, включим такую доработку в наш план работ.
Нужно пояснить, почему у нас есть simple_mtsafe и simple_not_mtsafe, и что вообще означает защита SObjectizer-а от многопоточности.
В принципе, есть две ситуации, когда нам может потребоваться однопоточный SObjectizer:
Мы видим задачу инфраструктуры simple_mtsafe в том, чтобы минимизировать накладные расходы SObjectizer-а, но при этом сохранить способность SObjectizer-а работать в многопоточном приложении. Так, в simple_mtsafe SObjectizer будет использовать всего одну рабочую нить вместо трех-четырех, как в случае с инфраструктурой default_mt. Но при этом пользователь может создать в своем приложении столько дополнительных рабочих потоков, сколько ему нужно, имя при этом возможность взаимодействовать с SObjectizer-ом из этих потоков.
Основное применение simple_mtsafe мы видим в разработке небольших GUI-приложений, в которых разработчик хочет вынести часть своей логики на дополнительный поток, в котором будет крутиться SObjectizer. При этом главный поток приложения останется доступным для обслуживания связанных с GUI операций.
А вот инфраструктура simple_not_mtsafe нужна только для случаев, когда пользователь хочет иметь именно что однопоточное приложение, в котором должен существовать один-единственный рабочий поток, на котором выполняются вообще все действия приложения.
Соответственно, основное применение simple_not_mtsafe мы видим в небольших утилитах, с более-менее сложной логикой внутри, но в которых важна экономия ресурсов. В легковесных процессах-worker-ах. А также в приложениях для совсем слабеньких платформ.
Как раз в том, что инфраструктура simple_not_mtsafe предназначена только и исключительно для однопоточных приложений, кроется принципиальное различие в реализациях simple_mtsafe и simple_not_mtsafe: инфраструктура simple_mtsafe вынуждена защищать свои «потроха» mutex-ом. Тогда как simple_not_mtsafe не нужно этого делать.
В итоге основные циклы работы инфраструктур simple_mtsafe и simple_not_mtsafe очень похожи, а отличаются они присутствием работы с std::mutex в случае simple_mtsafe. Код для simple_mtsafe:
template< typename ACTIVITY_TRACKER >
void
env_infrastructure_t< ACTIVITY_TRACKER >::run_main_loop()
{
m_activity_tracker.wait_started();
std::unique_lock< std::mutex > lock( m_sync_objects.m_lock );
for(;;)
{
process_final_deregs_if_any( lock );
perform_shutdown_related_actions_if_needed( lock );
if( shutdown_status_t::completed == m_shutdown_status )
break;
handle_expired_timers_if_any( lock );
try_handle_next_demand( lock );
}
}
И для simple_not_mtsafe:
template< typename ACTIVITY_TRACKER >
void
env_infrastructure_t< ACTIVITY_TRACKER >::run_main_loop()
{
m_activity_tracker.wait_started();
for(;;)
{
process_final_deregs_if_any();
perform_shutdown_related_actions_if_needed();
if( shutdown_status_t::completed == m_shutdown_status )
break;
handle_expired_timers_if_any();
try_handle_next_demand();
}
}
Примечание. В методы simple_mtsafe-инфраструктуры (вроде process_final_deregs_if_any() и try_handle_next_demand()) передается ссылка на std::unique_lock для того, чтобы можно было отпустить mutex на время выполнения соответствующих операций, после чего захватить его вновь.
Правда, работа с std::mutex в simple_mtsafe не обходится бесплатно. Эффективность simple_mtsafe-инфраструктуры на синтетических бенчмарках вроде ping-pong-а, оказывается на 25%-30% ниже, чем у инфраструктур default_mt и simple_not_mtsafe. Что вполне ожидаемо.
Версия 5.5.19, в которой реализованы инфраструктуры default_mt, simple_mtsafe, simple_not_mtsafe, доступна для загрузки на SourceForge [2]. Там же есть соответствующая документация [3].
В настоящий момент инфраструктура simple_not_mfsafe не имеет собственного mutex-а только для собственного основного рабочего цикла и связанных с этим структур данных (например, не защищены mutex-ами списки готовых к окончательной дерегистрации коопераций и таймерные заявки). Однако, в других частях SObjectizer-а различные примитивы синхронизации (вроде mutex-ов и spinlock-ов) все-таки присутствуют. Например, внутри каждого агента есть spinlock, который для simple_not_mtsafe в принципе не нужен, но свое место внутри класса agent_t занимает.
Это произошло потому, что по предварительным оценкам попытка убрать из внутренностей SObjectizer-а все связанные с синхронизацией объекты для инфраструктуры simple_not_mtsafe, могла бы затянуть работу над версией 5.5.19 еще, как минимум, на несколько месяцев. Чего нам сильно не хотелось.
Не хотелось так же и ломать совместимость между версиями SObjectizer, что было бы неизбежно, если бы мы попробовали перейти на использование шаблонной магии для более эффективной реализации simple_not_mtsafe. Например, одна из идей была в том, чтобы тип инфраструктуры для агента задавался параметром шаблона. Тогда бы пришлось описывать свои классы агентов как-то так:
template<typename ENV_INF>
class my_agent : public so_5::agent_t<ENV_INF> {
...
};
А это обязательно сломало бы совместимость и существенно затруднило бы перевод старого кода на новые версии SObjectizer.
Поэтому мы решили в версии 5.5.19 оставить уже существующие объекты синхронизации как есть, а над способом их изъятия для simple_not_mtsafe подумать при разработке следующей версии. Вот, начинаем думать. Если кому-то кажется, что это очень важная штука, то дайте знать, начнем думать интенсивнее ;)
Дабы продемонстрировать, куда это все может завести в пределе, мы попробовали запилить пример примитивного однопоточного HTTP-сервера, в котором асинхронная обработка запросов делегируется SObjectizer-у [4]. При этом и HTTP-сервер (на базе парсера от NodeJS и Asio), и SObjectizer сообща работают на единственной главной нити приложения. Вроде работает. Правда, сопутствующие технологии, вроде restinio [5] (наш асинхронный HTTP-сервер) и so_5_extra (позволяет совместно жить на одной нити SO-5 и Asio) пока еще не достигли продакшен качества. Но мы над этим работаем.
Работа над версией 5.5.19 заняла намного больше времени, чем мы сами ожидали, хотя виной тому вполне объективные причины [6]. Надеемся, что следующую версию, 5.5.20, работа над которой, по сути, уже началась, мы сможем выкатить намного оперативнее. Сейчас формируется что-то вроде wish-list-а для новой версии [7]. Ну и, соответственно, у читателей есть возможность повлиять на функционал SObjectizer-а. Напишите нам в комментариях, что бы вы хотели видеть в SObjectizer-е. Или, напротив, чего бы вы видеть не хотели. Или может вам что-то мешает использовать SObjectizer?
Мы очень внимательно прислушиваемся к тому, что нам говорят. Так, в свое время мы избавились от пространство имен so_5::rt и добавили такие фичи, как приоритеты агентов, иерархические конечные автоматы и мутабельные сообщения именно благодаря обсуждениям SObjectizer-а на различных профильных ресурсах и не только. Посему есть вполне реальный шанс сделать из SObjectizer-а нужный для вас инструмент, только чужими руками :)
Автор: eao197
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/255491
Ссылки в тексте:
[1] в недавно вышедшей версии 5.5.19: https://sourceforge.net/p/sobjectizer/news/2017/05/sobjectizer-v5519-released/
[2] доступна для загрузки на SourceForge: https://sourceforge.net/projects/sobjectizer/files/sobjectizer/SObjectizer%20Core%20v.5.5/
[3] документация: https://sourceforge.net/p/sobjectizer/wiki/so-5.5.19%20Environment%20Infrastructure/
[4] пример примитивного однопоточного HTTP-сервера, в котором асинхронная обработка запросов делегируется SObjectizer-у: http://eao197.blogspot.com.by/2017/05/progc-http-so-55-restinio-asio.html
[5] restinio: https://bitbucket.org/sobjectizerteam/restinio-0.2
[6] вполне объективные причины: https://sourceforge.net/p/sobjectizer/blog/2017/05/why-there-is-so-big-pause-between-5518-and-5519-releases/
[7] wish-list-а для новой версии: https://sourceforge.net/p/sobjectizer/discussion/550088/thread/31dbc561/
[8] Источник: https://habrahabr.ru/post/328872/
Нажмите здесь для печати.