воскресенье, 24 мая 2020 г.

[prog.c++] Особенность привязки агентов к диспетчеру, о которой полезно знать в SObjectizer

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

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

Этой штукой являются SObjectizer-овские диспетчеры. ИМХО, диспетчеры в SObjectizer -- это одна из отличительных черт нашего фреймворка, можно сказать, один из краеугольных камней в его основе.

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

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

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

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

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

Вполне логично сделать так, чтобы work_manager владел очередью запросов, т.е. эта очередь была бы частью его приватного состояния.

Однако, со временем мы можем прийти к тому, что work_manager будет обрастать все большей и большей функциональностью. Так, мы можем решить, что время нахождения заявки в очереди нужно ограничивать. И у work_manager-а может появится периодическое сообщение, при получении которого work_manager начнет выбрасывать те заявки, которые ждут слишком долго. Потом окажется, что отправитель заявки может захотеть проверять статус заявки и work_manager-у придется начать обрабатывать новое сообщение get_operation_status. Затем может оказаться, что отправитель запроса может захотеть отменить заявку. И work_manager-у придется начать обрабатывать еще одно новое сообщение revoke_operation. А потом мы можем захотеть, чтобы work_manager начал собирать статистику о том, сколько заявки ждут в очереди. И т.д., и т.п.

По мере усложнения агента work_manager мы можем обнаружить, что объем и сложность кода work_manager постоянно растет и мы ничего не можем с этим поделать, т.к. только агенту принадлежит очередь заявок. И со временем нам придется столкнуться с тем, что work_manager вырос в объеме до 3-4-5K строк кода, и вынужден обрабатывать несколько десятков разнообразных сообщений.

Что собенно плохо в ситуациях, когда какая-то новая функциональность добавляется в work_manager только в рамках эксперимента. Ну, например, нам показалось, что поддержка get_operation_status -- это хорошая идея. Мы ее реализовали, попробовали использовать. И достаточно быстро выяснилось, что в реальных сценариях get_operation_status практически не используется, а даже там, где это пытаются использовать, результаты получаются так себе, поскольку в условиях тотальной асинхронности get_operation_status может приносить устаревшие результаты. Поэтому от get_operation_status следует отказаться.

И вот добавление поддержки get_operation_status в более-менее сложный work_manager, а потом изъятие get_operation_status из work_manager, -- это трата времени, да еще и чревато возникновением каких-то ошибок.

В таких ситуациях упростить себе жизнь можно за счет того, чтобы сделать очередь заявок разделяемым объектом. К которому могли бы иметь доступ сразу несколько агентов.

Тогда мы могли бы иметь work_manager-а, который наполняет очередь и следит за отправкой запросов на обработку. И могли бы иметь агента operation_status_monitor, который бы обратывал get_operation_status.

Каждый из этих агентов работал бы "независимо" от другого. И мы могли бы добавлять новых агентов, а так же изымать старых когда нам этого захочется, без существенной переделки всех остальных агентов. Поскольку они вообще могут не знать друг от друге. Связывали бы этих агентов только общие разделяемые данные.

Но ведь разделяемые данные -- это плохо?

Да, плохо. Особенно когда сущности, которым требуются общие данные, работают на разных рабочих контекстах.

А вот в SObjectizer-е мы можем директивно заставить агентов работать на общем контексте.

Так, мы запросто можем создать one_thread диспетчер и привязать к нему как work_manager, так и operation_status_monitor, так и любого другого агента, которому потребуется доступ к очереди заявок.

И никаких проблем с многопоточностью здесь не будет. Т.к. для привязанных к одному one_thread диспетчеру агентов этой самой многопоточности и нет.


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

Тем не менее, знать про такую возможность при использовании SObjectizer-а полезно. В конце-концов диспетчеры в SObjectizer-е как раз и были сделаны для того, чтобы дать возможность пользователю контролировать где и как его агенты будут работать. И привязка нескольких агентов к одному общему one_thread-диспетчеру для того, чтобы они могли безопасно использовать разделяемые данные, -- это и есть пример такого контроля.

Но, повторюсь, нужно такое изредка.

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

Отправить комментарий