! | Данная информация предназначена только только для IT-специалистов по системной интеграции модулей БИОСОФТ-М. (см. Руководства пользователя к программным продуктам) |
Предположим, что возникла задача, выполнение которой надо распараллелить.
Как поступают те, кому наплевать на надежность продукта в этой ситуации? Они запускают триды в приложении и тонет в фатальном крушении нормальной логики.
Что мы делаем? Юзаем служебный параллельный процесс связи которого с основным приложением автоматизированы механизмом ParaService. И затем наслаждаться отсутствием взаимных блокировок, коррупций глобальных и мемберных данных, фаталов в нереентерабельных библиотеках и необходимости мучительно продумывать все возможные миллионы взаимно перепутанных последовательностей параллельного выполнения операций в методах одного и того же объекта.
ParaService это процесс, который своим поведением максимально эмулирует типовое применение трида.
Он не является самостоятельным приложением в системе а функционирует в подчиненной роли, выполняя фоновые задачи для запустившего его процесса. Он терминируется когда главное приложение перестает нуждаться в данном сервисе.
Дополнительная особенность парасервисов, делающая их более похожими на сервисы/демоны ОС в том, что несколько прикладных процессов могут использовать один процесс парасервиса. (например несколько приборных приложений запущенных одновременно могут использовать общий сервис не конфликтуя за единственный возможный эксклюзивный хендл для доступа к прибору)
Islib-ParaService это обычный процесс с точки зрения ОС и не имеет никакого отношения к Windows-сервисам.
ParaService для параллельных задач
Приложение захотело запустить задачу в параллельном сервисе.
Оно формулирует параметры задачи, заполняет их в проперти коммуникационного объекта и посылает (Post) пакет сервису.
Сервис в этот момент может быть уже запущен данным или другим приложением тогда он начнет отрабатывать задачу сразу.
[ App ]---Args--->[ Post ]---Args--->[ ParaService Process ]
Если же парасервис не запущен, то он будет запущен библиотекой автоматически. И получит посланные ему параметры когда загрузится и будет готов к работе.
[ App ]---Args--->[ Post ]---Run---> MyParaService.exe ... [ Post ]---Args--->[ ParaService Process ]
Каждый сервис идентифицируется уникальным строковым идентификатором и в один момент времени в системе может быть только один запущенный парасервис с определенным ID.
Если несколько приложений посылают параметры парасервису с одним и тем же ID, то новых сервисов запускаться не будет, будет использоваться процесс, запущенный первым приложением при самом первом посте. Однако этот процесс получит посты и от всех остальных приложений.
[ AppA ]---ID_1--- | [ AppB ]---ID_1---+--->[ ParaService(ID_1) ] | [ AppC ]---ID_1--- | '----ID_2------->[ ParaService(ID_2) ]
Но ничто в принципе не мешает (кроме ограничений на ресурсы ЭВМ!) запускать множество сервисов одного типа но в разных процессах выполняющие задачи независимо для разных клиентов. Такие сервисы могут быть запущены из одного и того же экзешника и использовать одни и те же коммуникационные классы, но будут использовать разные ID.
--- Multiservice.exe ---------------------------- | | [ App ]---ID_1--------> Handler for ParaService(ID_1) in process 1 | | | [ App ]---ID_2--------> Handler for ParaService(ID_2) in process 2 | | | -------------------------------------------------
Ниже будут подробно расписаны все интерфейсы, правила и процедуры. Для разделения зон ответственности между приложением и системными библиотеками кратко перечислим функции тех и других:
Запуск, отслеживание жизненного цикла процесса и начальные указания парасервису реализованы как расширение интерфейса Безопасная посылка данных процессам через Interproc-Post
Нужно изучить его сначала а потом уже вникать в особенности его использования для запуска сервисов.
Как и в обычной межпроцессной коммуникации посылка представлена объектом, унаследованным от CInterprocPostIfaceGp. Но отправка сообщения парасервису реализуется не обычным методом PostBroadcast() а особым RunParaServicePost(). Такая посылка влечет за собой следующие отличия:
1) Данные поста это не последовательные команды а обновление состояния.
2) Посылка последнего поста будет повторена автоматически при рестарте сервиса.
3) Посылка не запущенному сервису будет отложена и выполнена только после его старта.
4) Если делается посылка не запущенному сервису то он автоматически запускается.
5) Объект посылки парасервису (CInterprocPostIfaceGp) должен хранится приложением так как его уничтожение терминирует процесс сервиса (это в отличие от создания временного объекта для обычного поста).
6) Все посылки одному и тому же сервису должны делаться через один и тот же хранимый приложением объект (CInterprocPostIfaceGp).
Это концептуальный момент который надо держать в голове проектируя парасервис а не техническая часть интерфейса.
Важное отличие применения Безопасная посылка данных процессам через Interproc-Postк парасервисам в том, что отдельные посылки не ставятся в очередь в качестве команд которые должен отработать параллельный процесс.
Вместо этого каждая посылка определяет ТЕКУЩЕЕ СОСТОЯНИЕ в котором должен находится парасервис. В начальный момент эти данные можно рассматривать как расширенную "командную строку". Затем клиенты могут обновлять содержимое такой "командной строки" посылая измененные посты.
Такая необычная интерпретация постов следует из того, что сервис должен иметь возможность рестартовать в ЛЮБОЙ (!) момент работы и при (ре)старте получить от клиентов всю информацию о том чем он должен заниматься.
Вспоминаем как посты через Interproc-Post приложение посылает само и они отправляются однократно и забываются.
В отличие от этого посты парасервисам ПЕРЕПОСЫЛАЮТСЯ АВТОМАТИЧЕСКИ ПРИ СТАРТЕ/реСТАРТЕ ПАРА-ПРОЦЕССА.
Это критический для понимания момент в коммуникации из которого следует все остальное!
Обычный сценарий применения Interproc-Post подразумевает, что посты отсылаются когда принимающий(ие) их процесс(ы) уже запущены - посты парасервису могут быть подготовлены до того как процесс запустится.
Для отправки постов парасервису используется функция
rMyPostObject-> RunParaServicePost( sServiceId, sInstanceId, eParaServiceDataStatus, out sError)
вместо обычного PostBroadcast().
RunParaServicePost() может не только послать данные но и она же и запускает парасервис если он еще не запущен.
Соответственно если парасервис уже запущен то он получит пост.
Если на момент вызова RunParaServicePost() парасервис еще не был запущен или еще не загрузился, или процесс загрузился но не инициализировал коммуникацию то пост будет отложен до момента готовности парасервиса принять его.
И если парасервис вдруг рестартует по какой бы то ни было причине то новый экземпляр его процесса по готовности снова получит тот же самый последний пост посланный RunParaServicePost().
Это все гарантирует получение инициализированным парасервисом всех данных, посланных ему последний раз каждым из его клиентов.
И последняя важная особенность RunParaServicePost():
Объекты постов посылаемых парасервису приложение должно хранить все время пока парасервис должен продолжать свою работу. И через один и тот же объект поста можно (и нужно! перепосылать пост многократно меняя данные параметры которые надо передать сервису.
Иначе говоря объект поста (унаследованный от CInterprocPostIfaceGp) в случае применения к нему RunParaServicePost() становится инкапсулятором парасервиса и хранилищем его текущего требуемого состояния с точки зрения клиентского приложения.
Именно срок жизни всех ref<CInterprocPostIfaceGp> посылающих параметры парасервису и определяет срок жизни процесса парасервиса. Как только парасервис обнаружит что все его клиенты уничтожили объекты посылающие ему параметры парасервис тут же автоматически терминируется.
Иногда (Uport) бывает нужно только запускать процесс парасервиса без передачи ему данных. Или в случае если параметры работы парасервиса меняются редко, а проверять запущен ли процесс хочется часто. Для этого RunParaServicePost() имеет параметр
EParaServiceDataStatus eParaServiceDataStatus
выбирающий логику:
enum EParaServiceDataStatus { // this always forces a post to be queued for sending to the ParaService E_ParaServiceDataStatus_SendUpdatedPost, // this avoids needless posting when no data is changed, // use when you want to just meke sure the ParaService process is running E_ParaServiceDataStatus_NoUsefulDataChange, };
Так как RunParaServicePost() не только посылает данные сервису но и запускает его по мере необходимости, то он должен знать какой экзешник ему нужно запускать.
Для этого в объект CInterprocPostIfaceGp добавлен
ref<CInterprocRunParaServiceIfaceGp> _x_rInterprocRun
(см. подробное описание запуска процессов в Запуск и выгрузка параллельных процессов )
Из всех параметров запуска процесса в случае с парасервисом нам нужно указать только путь к .exe файлу:
rPost->_x_rInterprocRun->x_pathRunExe = ...
Другие параметры (как дополнение командной строки) трогать не рекомендуется чтобы не создавать опасной неоднозначности. Приложение ведь не должно заботить будет ли данная отсылка запускать новый процесс или передаст данные уже существующему. По этому мы не знаем будут ли наши параметры запуска процесса использоваться или нет. А потому не надо их трогать, а если нужно что то сообщить сервису, то передавать это следует в данных поста или другим надежным и однозначным способом.
Для обеспечения лучшей инкапсуляции как правило приложение будет запускать свой собственный экзешник в роли сервиса либо DLL будет запускать свой стартер чтобы экземляр этого DLL отработал задачу в составе параллельного процесса. Поэтому путь к запускаемому парасервису обычно будет
// restart our whole app in ParaService mode rPost->_x_rInterprocRun->x_pathRunExe = CProject::GGetGlobalInstance().GetProjectStarterExePath();
Еще один небольшой нюанс отличающий RunParaServicePost() от PostBroadcast()..
Если PostBroadcast() отсылает сообщение немедленно, и оно насколько это возможно оперативно будет принято получателями то RunParaServicePost() на самом деле ВСЕГДА откладывает момент отсылки и делает его по таймеру.
В сочетании с тем фактом что приложение ПОСТОЯННО ХРАНИТ данные поста для RunParaServicePost() получается что оно может изменить отсылаемые данные ПОСЛЕ вызова RunParaServicePost(). Что не возможно при применении PostBroadcast()..
Это сделано специально чтобы логика подготовки данных для поста парасервису срабатывала всегда идентично как в случае если RunParaServicePost() постит в уже запущенный сервис так и в случае если ему нужно сначала запустить этот сервис и только потом послать пост.
возможно в следующих ситуациях:
(это произойдет в случае если для всех коммуникационных объектов вызовутся деструкторы во всех клиентских процессах)
В этой ситуации процесс парасервиса терминирует сам себя.
(для этого watch dog timer (WDT) ослеживается автоматически библиотеками)
В этом случае будет запущен новый процесс сервиса.
Однако в этом случае если какой бы то ни было из клиентов выразит желание продолжать иметь соответствующий сервис (обновив его параметры через посылку очередного Post сообщения) то для продолжения работы сервиса запустится новый процессс.
В любой ситуации перезапуска процесс парасервиса получит посылку с параметрами которые клиентский процесс (или процессЫ) посылал(и) ему последний раз. Это может быть достаточно чтобы аосстановить контекст работы и продолжить прерванную задачу.
ParaService принимает обновления своего состояния в виде постов по аналогии с Interproc-Post.
Объект хандлера onpost нужно хранить все время пока сервис запущен. Его следует создать как мембер класса некоего глобального инкапсулятора парасервиса:
// This Post handler must adhere to PFN_Handler signature in onpost template<> // This servies as a ParaService handler. void Handle<...>Post( ref<C<...>Post> rPost); onpost<Handle<...>Post> _m_onpost<...>;
Например для парасервиса UportParamod:
// This Post handler must adhere to PFN_Handler signature in onpost template<> // This servies as a ParaService handler. void HandleUportParamodPost( ref<CUportParamodPost> rUportParamodPost); onpost<HandleUportParamodPost> _m_onpostUportParamod;
Вместо простенькой бессбойной функции Interproc-Post OpenPostOffice() для открытия ParaService используем метод
onpost::TryOpenParaServicePostOffice(sId, this)
Он вернет нам true если данный процесс запущен именно для обслуживания сервиса с указанным sId. Возвращаемый TryOpenParaServicePostOffice() bool является ключем к принятию решения о том в какой роли дальше будет работать приложение.
Можно так же узнать запущен ли процесс в режиме парасервиса без аллокирования хандлера вызовом
bool IsParaServiceHandlerProcessFor(sPostIfaceName)
для временного объекта ref<CInterprocParaServiceIfaceGp>().
Колбак функция вызываемая при получении приложением постов - 'Handle<...>Post(ref<C<...>Post> rPost); реализуется точно так же как и обычный хандлерInterproc-Post. С той только логической разницей что в посте приходящем парасервису приходят не инкрементные команды а полное описание задачи которую в данный момент должен выполнять сервис.
Функция хандлера постов для парасервиса будет вызываться не только когда клиент сделает посылку, но и обязательно она будет вызвана первый раз сразу после инициализации сервиса. При этом она получит те данные, которые клиент посылал сервису последний раз.
Если TryOpenParaServicePostOffice() вернул true то значит нам надо менять роль нашего экзешника подразумевая что он не самостоятельное приложение а выполняет функции сервиса.
В частности в LoaderImpl::OnStartApplication() или его аналоге для режима парасервиса исключаем инициализацию главного окна. UI сервису не нужно (возможно только специальное скрытое от конечного пользователя отладочное UI).
См. Применение ParaService на практических примерах.
Есть глобальная функция IsParaServiceProcess() выбирать между которой и IsParaServiceHandlerProcessFor() надо осторожно.
Досадные проблемы будут если подразумевать что модуль может обслуживать только один сервис и использовать эти функции как взаимозаменяемые. Они такими не являются но это не очевидно пока не возникнет нескольких возможных сервисов для одного и того же модуля!
1) В случае когда процесс парасервиса автоматически завершается при исчезновении его клиентов он это делает сам. Библиотеки отслеживают клиентов по таймеру и делают PostQuitMessage().
В этом случае процесс не будет прерван неожиданно, а только когда отдаст управление своему циклу обработки сообщений.
Под отладчиком этот вид терминации не происходит. Это позволяет запустить парасервис из IDE и затем запустить основной процесс (из второго IDE) и отлаживать их совместную работу. Вовне отладчика можно применить debslot("Interproc.ParaService.LiveWithoutClients?").
2) Если процесс парасервиса завис, то его терминируют клиенты, детектировавашие зависание. Произойдет это если WDT не будет обновлятся библиотеками в парасервисе в течении заданного таймаута. Обновляют они его автоматически по своему внутреннему таймеру. Соотвественно зависанием будет признана ситуация когда код парасервиса долго не возвращает управление циклу обработки сообщений.
Терминация такая может произойти на пол пути выполнения парасервисом какой то неожиданно долгой операции без выполнения обычных процедур завершения процесса, без сохранения UniConfig конфига (который вообще не нужен парасервису).
Естественно если процесс остановлен на брейкпоинте таймаут выйдет и по этому под отладчиком авто-терминация автоматически отменяется. При запуске не под IDE того же эффекта достигает debslot("Interproc.ParaService.NoKillOnStall?").InputBool().
3) Парасервис как и любой процесс может в принципе завершить и себя по PostQuitMessage(). Но это не типовая ситуация. Процесс сервиса как правило должен продолжать работать покуда существуют клиентские объекты запустившие его. Основанием для самостоятельной терминации может быть фатальный сбой после которого процесс сервиса не может продолжать работу и может надеятся что клиенты его перезапустят снова. Более хитрая теоретическая ситуация когда процесс парасервиса выполнив всю свою работу может завершать себя после продолжительного простоя чисто ради того чтобы освободить память и ресурсы системы. В расчете опять же, что будет перезапущен клиентами когда снова понадобится. Естественно такой подход замедлит и добавит латентности при перезапуске сервиса.
4) Если сервис запустился а клиентов у него нет, то он терминируется автоматически. Такой случай может возникать, если клиентское приложение, запустившее сервис закрыли до того как сервис загрузился.
Тот же самый глобально уникальный строковый ID который применяется в Безопасная посылка данных процессам через Interproc-Post используется и для идентификации парасервиса.
Только один процесс соответствующий InterProc ID может быть запущен в системе в данный момент времени. Поэтому для парасервисов есть еще опциональный параметр sInstanceIdпередаваемый RunParaServicePost(). Если он не пустой, то добавляется к ID сервиса позволяя запускать несколько процессов одного типа. В инициализации сервиса ничего при этом не меняется. Сервис может узнать свой sInstanceId из GetOpenedInstaceId() если ему нужно знать что он не один обслуживает данный тип сервиса в системе.
Любое количество клиентских процессов главных приложений (C_nMaxClientsForParaService) может использовать парасервисный процесс данного класса и даже с одним и тем же sInstanceId. Важно следить чтобы они не засыпали сервисы конфликтующими командами! Либо всегда добавлять к sInstanceId уникальный для данного клиентского процесса идентификатор чтобы каждый клиент общался только со своими экземплярами сервисных процессов.
На количество независимых парасервисов с разными ID в данный момент ограничений не накладывается.
(см. также Применение ParaService на практических примерах)
Для примера рассмотрим задачу, которая идеально и наглядно ложится на данные принципы.
Предположим для приложения требуется в параллельном режиме на втором мониторе выводить заданную анимацию (пример из Stabip).
Содержимое анимации меняется при переходе к следующему этапу приложения.
В данном случае приложение создает и постоянно хранит объект с параметрами поста и посылает новый пост заполнив его информацией о требуемой анимации при переходе к очередному этапу на котором анимацию нужно изменить.
void HandleNextStage() { // update service parameters m_rPostToAnimator->x_rAnimation = GetCurrentAnimation(); // our .exe handles the service m_rPostToAnimator->_x_rInterprocRun->x_pathRunExe = sys::GetExeFilePath(); // send or start-and-send m_rPostToAnimator->RunParaServicePost(C_sMyAnimator, out sExeStartError); }
В этом случае приложению больше ничего не нужно делать и ни о чем заботится.
Внутренняя реализация ParaService сама автоматически:
Другой пример - процесс вычитывающий данные из Unimod. Ему вообще не нужно на входе никаких параметров. Пока он работает - заполняет shared буфера данными. Если клиентам они нужны они эти данные вычитывают. При (ре)старте такой процесс сам знает как детектировать устройство и продолжить процедуру.
Более сложные ситуации с приборными процессами. используемыми потенциально несколькими приложениями отдающими прибору команды и ожидающие поддержание прибора и сервиса в определенном состоянии.
Базовый механизм для такого сервиса здесь реализован а прикладные детали - очереди, контексты, транзакции и конфликты на прикладном уровне должны разрешаться приложением за пределами данного топика.
Если схема такой посылки постов кажется сложной или не удачной для вашего конкретного приложения, то данных с этими постами можно и не посылать. Можно посылать ПУСТЫЕ посты парасервису, используя расширенный механизм RunParaServicePost только для того чтобы стартовать парасервис и контролировать время его жизни.
При этом всю остальную коммуникацию можно организовать обычными постами, или через общую память, файлы и т.п. Однако это не избавляет от необходимости помнить что такая коммуникация должна обеспечивать корректное возобновление работы сервиса при прерывании его в любой момент времени.
Нет никаких конфликтов между одновременным использованием обычных Interproc постов и специальными постами, запускающими парасервис.
Если это необходимо, то можно добавить любые каналы Post-коммуникации между сервисами и их клиентами в любых направлениях. В т.ч. полностью заменив ими пересылку параметров через RunParaServicePost().
Тогда RunParaServicePost() необходимо вызывать только чтобы клиент засвидетельствовал факт того, что он хочет иметь в наличии заданный сервис. Параметров передавать не обязательно.
Однако нужно помнить что PostBroadcast() посланный в момент, когда процесс еще не запустился будет утерян. RunParaServicePost() гарантирует отсылку и повторную переотсылку последнего пакета запущенному и перезапущенному процессу.
Рекомендуется сразу, по крайней мере для удобства отладки, реализовывать функции парасервиса не только работающими в отдельном процессе но и опционально в рамках основного.
Будет это вотребовано в релизе или нет, но понадобится сравнительное тестирование того как функции работают в паралель по сравнению с синхронным режимом и может быть востребован аварийный или ограниченый режим когда (в простых случаях?) задачи парасервиса проще, надежней и быстрей выполнить в основном процессе. И естственно отлаживать один процесс проще чем два.
Для реализации этого не нужно перемешивать инфраструктуру классов реализующих парасервис с прикладными классами, реализующими его прикладную функциональность.
// This returns to the main app // - either local implementation residing in the current process // - or parallel implemetation operating via ParaService posting // according to its internal settings. // The app does not need to care about the details! ref<CStabipDizzyRenderIface> NewStabipDizzyRender() new virtual auto; ... if (debslot("Dizzy.InProcess?").InputBool()) { // default implementation return ref<CStabipDizzyRenderIface>(); } else { return m_rStabipDizzyRenderAsPoster; } // Option 1: As ParaService: void CStabipDizzyRenderAsPoster::OnStopRendering() { m_rStabipDizzyPost->x_bEnableRendering = false; RepostStabipAnimationStateToParaService(); } // ...... in the second process: void CStabipDizzyServiceImpl::HandleStabipDizzyPost( ref<CStabipDizzyPost> rStabipDizzyPost) { if (rStabipDizzyPost->x_bEnableRendering) { ... } else { m_rStabipDizzyRender-> StopRendering(); } } // Option 2: All in the same process: void CStabipDizzyRenderImpl::OnStopRendering() { this->x_rRenderingDbStabipStepConfig = null(); m_bForceSecondMonitor = false; UpdateRenderWindowSituation(); }
Механизм ParaService специально спроектирован для гибкой связи между основным и сервисным процессами не зависящей от того какой из процессов изначально кого запустил.
Клиентский процесс не только использует сервисы запущиенные другими процессами (при совпадении ID) но и даже когда сам запускает процесс ParaService не ожидает что запущенный им процесс это будет тот сервис с которым он установит коммуникацию.
Стартер .exe указанный для запуска парасервиса может реально не обслуживать сервис а в свою очередь запускать процесс реализующий сервис. Либо может посылать команду другому процессу, в свою очередь запускающую парасервис.
Главное - чтобы ID указанный в командной строке поступил на вход процессу реализующему в итоге данный сервис. Достаточно чтобы в итоге в системе один из процессов инициализировал коммуникацию, объявив себя требуемым парасервисом.
Возможен даже вариант когда процесс парасервиса запускает процесс который станет его клиентом.
Если парасервис попробует запустить свой собственный сервис, то только установит связь с со своим же собственным процессом.
Для обеспечения такой гибкости реализация процедуры старта ParaService не подразумевает что process ID запущенный ей вообще имеет какое то отношение к сервису. Связь с процессом ParaService устанавливается между ним и запустившим его клиентом как и с другими его клиентами по принципу ослеживания статуса и внутренних ссылок в специальной разделяемой области памяти. Запущенный парасервис после удачной инициализации, убедившись в своей уникальности глобально публикует для клиентов необходимые для коммуникации данные. Клиенты замечают это (отслеживая по таймеру) и понимают, что сервис запустился.
При этом системные идентификаторы процессов используются только для отслеживания терминации процессов и предотвращения повторного запуска таким образом что отношения родительских процессов и кто кого запустил не влияют на логику коммуникации.
Прокси стартер не должен завершаться по крайней мере раньше чем запущенный им парасервисный процесс инициализируется. По ID стартерного процесса отслеживается таймаут перед попыткой повторного рестарта сервиса. Если сервисный процесс не может стартовать вообще, то все клиенты будут пытаться перезапускать его с очень маленьким таймаутом. Это произойдет если стартерный процесс будет завершаться не дождавшись инициализации сервиса.
Для процесса ParaService библиотеки блокируют доступ к файлу конфига UniConfig. По этому при запуске он будет всегда иметь все параметры конфига в значениях по умолчанию.
Сервис должен получать все данные для своей работы от главного приложения и только в главном приложении должны быть все конфиги!
Если парасервис вдруг начнет генерить свои левые конфигурационные файлы в обход UniConfig, то важно помнить чтобы в паралельной среде не возникло конфликтов в том числе одновременного доступа к файлу из нескольких процессов, перезаписи устаревшими данными обновленных файлов, и конфликта между одним и тем же экзешником или DLL в разных экзешниках запущенных а разных парасервисах с разными ID.
Конфиг парасервиса в любом случае не учавствует в процедуре инсталляции и импорта.
на примере текущих применений сабжа: Применение ParaService на практических примерах
Не поддерживается реализация более одного парасервиса одним процессом.
Сейчас если процесс даже и мог бы обслуживать несколько сервисов, то инициализировать их нет возможности и нужно либо разносить разные классы сервисов по разным процессам. Однако один и тот же экзешник может запускаться несоклько раз и тогда обслуживать разные сервисы в разных процессах. Кроме этого можно оборачивать все сервисы процесса в один глобальный если это удобно по логике задачи.
Видится, что потребность объединять сервисы может возникнуть только в целях тонкой оптимизации ресурсов системы и памяти. Если острая потребность возникнет, то можно дать возможность указать в командной строке несколько имен сервисов сразу или дать дозапускать дополнительные сервисы в рамках существующего процесса. Нужно только будет усложнить процедуры отслеживания повторных экземпляров процесса и реализовать глобальный мапинг всех сервисов процесса (класс под такой заготовлен но не используется).