Эта статья является адаптированным вариантом текста одноименного доклада с конференции C++ CoreHard Autumn 2017. Статья рассматривается как завершение темы, поднятой ранее в публикациях "Модель Акторов и C++: что, зачем и как?" и «Шишки, набитые за 15 лет использования акторов в C++» часть I и часть II. Сегодня речь пойдет о том, как понять, что Модель Акторов может быть успешно использована в вашем проекте.
В принципе, статья является «капитанской», т.к. описываемые в ней вещи вполне очевидны и диктуются обычным здравым смыслом. Но, к сожалению, не часто внимание акцентируется именно на них.
Лирическое отступление на тему «Модель Акторов и C++: миф или реальность?»
В статье обсуждаются вещи, которые присущи самой Модели Акторов, безотносительно конкретных языков программирования. Но, т.к. автор тесно связан с разработкой ПО на C++, то есть некоторый акцент на применимость акторов именно в C++.
В Интернете много информации о том, как применяется язык программирования Erlang или фреймворк Akka. А вот о том, как используется Модель Акторов в С++ информации мало. И может показаться, что Модель Акторов и С++ в принципе не совместимы.
Это не так. Модель Акторов может успешно применяться и в С++. И, что характерно, она таки применяется. Вот небольшой перечень прикладных областей, для которых мне известны примеры использования Модели Акторов в С++:
- промышленная автоматизация (АСУ ТП);
- телеком;
- электронная и мобильная коммерция;
- имитационное моделирование;
- САПР;
- игростроение;
- ПО промежуточного слоя (СУБД, ...)
Есть даже несколько готовых, живых и развивающихся фреймворков для C++, вот наиболее известные из них:
- QP/C++ (двойная лицензия);
- Just::Thread Pro: Actors Edition (коммерческая лицензия);
- С++ Actor Framework (BSD-3-Clause лицензия);
- SObjectizer (BSD-3-Clause лицензия).
Кроме того:
- OOSMOS (двойная лицензия, язык C, но может использоваться и в C++);
- Asyncronous Agents Library (входит в Visual Studio);
В принципе есть из чего выбрать. Можно даже не писать свой велосипед. Хотя мы, С++ программисты, это очень любим. Однако, будучи старым разработчиком одного из этих фреймворков, могу сказать две вещи:
- во-первых, Модель Акторов можно смело использовать в C++. Это приносит свои положительные результаты. Неоднократно проверено на практике;
- во-вторых, делать свою реализацию Модели Акторов для C++ — это неблагодарное занятие. Труда и времени вложить придется много. А окупится ли это в средне- или долгосрочной перспективе — неизвестно, скорее всего не окупится.
Так что если вам захочется использовать Модель Акторов в C++, то имеет смысл сперва попробовать что-то из уже готового. И только если ничего под вашу задачу не подойдет, тогда уже можно подумать на счет своего велосипеда. Или даже на счет смены языка программирования.
А стоит ли вообще разговаривать про Модель Акторов?
Основную часть рассказа следует начать с вопроса на миллион: «А нужна ли вообще Модель Акторов?»
Особенно часто такой вопрос возникает на профильных ресурсах. Там анонимные эксперты, которые знают и умеют все на свете, утверждают, что не нужна. Возможно, такие мегамонстры и не нуждаются ни в Модели Акторов, ни в других подходах к конкурентному программированию. Но мне интересен адекватный и взвешенный ответ на этот опрос.
И по-моему, вопрос «Нужна ли Модель Акторов?» очень похож на вопрос «Нужен ли самосвал на трассе Формулы-1?»
Суть в том, что и один, и второй вопрос лишены смысла без одного важного уточнения… А именно: "Для чего?"
Если мы зададим вопрос «Для чего нужен самосвал на трассе Формулы-1?», то мы сразу же получаем вполне осмысленное пространство для нормальных ответов. Например: для ремонта полотна трассы Формулы-1. Нужен ли для этого самосвал? Скорее всего — да. И вопрос обрел смысл, и нормальный ответ для него есть.
Точно так же и с нужностью Модели Акторов. Как только мы задаем вопрос «Для чего нужна Модель Акторов в таких-то условиях?», то мы сразу же получаем возможность найти осмысленный ответ.
И ответ этот зачастую будет: для того, чтобы упростить себе жизнь!
Однако...
Однако, не все так просто. Возможно, вы читали книгу «Вы, конечно, шутите, мистер Фейман!» Там был рассказ о том, как в лаборатории Принстона молодой Ричард Фейман пытался поставить игрушечный опыт и у него взорвалась большая бутыль с водой. Были испорчены фотографии с результатами предыдущих важных экспериментов. Руководитель лаборатории тогда сказал Фейману: «Эксперименты новичков должны проводиться в лаборатории для новичков!»
И эти же слова можно адресовать всем тем, кто хочет затащить в реальный проект технологию, с которой раньше никто не работал.
В принципе, мы это любим. Есть даже термин HDD — Hype Driven Development. Когда мы что-то новое узнали, вдохновились, затащили в боевой проект и долго и упорно боролись с последствиями.
Так вот, для того, чтобы Модель Акторов не причиняла невыносимую боль в большом проекте, нужно сперва потренироваться. Например, на кошках :)
Тренироваться нужно на кошках!
Попробуйте сперва Модель Акторов в каких-то мелких, игрушечных задачках. Посоздавайте акторов, поотсылайте между ними сообщения. Посмотрите, где вам понравилось, где не понравилось. Подумайте, почему не понравилось.
Очень часто в начале работы с акторами люди злоупотребляют сообщениями. Любую сущность в программе пытаются представить в виде акторов, а любое взаимодействие между ними — через асинхронные сообщения. Но это не всегда хорошо работает. Нужно на собственном опыте нащупать ту грань, когда преимущества асинхронных сообщений начинают переходить в недостатки асинхронных сообщений.
Если вы эту грань еще не нащупали, то притащив Модель Акторов в реальный большой проект вы, скорее всего, насоздаете лишних акторов, которые будут обмениваться ненужными сообщениями. Это станет для вас и лишней головной болью, и причиной проблем с вашим кодом.
В общем, все хорошо в меру, и эту меру лучше найти на игрушечных задачках, нежели в боевом проекте.
Модель Акторов как способ смотреть на мир
Когда мы беремся за Модель Акторов, то мы должны понимать, что Модель Акторов — это не только способ организации взаимодействия между сущностями внутри программы. Это еще и подход к анализу задачи и способ проектирования.
Здесь уместно провести аналогию с Объектно-Ориентированным Подходом. 25 или 30 лет назад объектный подход с тремя своими простыми принципами стал настоящим прорывом. Прорывом, который позволил не только упростить написание кода больших программных систем. Но, главное, объектный подход стал инструментом, который существенно упростил анализ и проектирование этих самых больших программных систем.
Принципы объектного подхода позволили смотреть на предметную область другими глазами. Люди учились особым образом классифицировать объекты своей предметной области. А это давало возможность более просто реализовывать объекты внутри программы.
Вот что-то похожее дает людям и использование Модели Акторов. Она сама базируется на трех простых принципах:
- актор – это некая сущность, обладающая поведением;
- акторы реагируют на входящие сообщения;
- получив сообщение актор может:
- отослать некоторое количество сообщений другим акторам;
- создать некоторое количество новых акторов;
- определить для себя новое поведение для обработки последующих сообщений.
Но эти принципы дают разработчику прежде всего способ иначе взглянуть на свою предметную область. Работая с Моделью Акторов мы начинаем видеть в предметной области не просто какие-то объекты с какими-то свойствами. Мы начинаем видеть объекты с собственным поведением. А так же способы общения этих объектов с другими таким же объектами.
Получается, что мы сперва учимся обнаруживать акторов в самой предметной области. А затем мы получаем возможность безболезненно перенести этих акторов в объекты внутри нашего кода.
И как раз этим Модель Акторов ценна: мы имеем возможность оперировать одинаковыми понятиями как по отношению к предметной области, так и по отношению к программной реализации.
Обратная сторона медали
Мы можем посмотреть на какую-то предметную область и не увидеть там акторов вообще.
Характерный пример: вычислительная математика. Там практически нечего представлять в виде акторов. Конечно, можно попытаться, но смысла в этом не будет. Например, можно сделать актор-матрицу и актор-вектор. И вектор будет отсылать матрице сообщение «умножь себя на меня». Но это даже звучит довольно глупо.
Какое-то подобие акторов может возникнуть при распараллеливании математических расчетов. Там возникают сущности, которые отвечают за распараллеливание и за сбор результатов. Эти сущности могут быть похожи на акторов. Но далеко не факт, что их выгодно делать акторами. Может проще использовать map-reduce или task based parallelism.
Так что есть предметные области, в которых использование Модели Акторов не только не приносит никаких бонусов, но может еще и усложнить нашу жизнь. В таких областях нет смысла использовать Модель Акторов. И если у вас такая область, то вам просто не нужен самосвал, вот и все.
Лакмусовые бумажки
Давайте посмотрим на несколько маркеров, наличие которых может подсказать вам, что Модель Акторов в вашей задаче приживется.
Сразу оговорюсь: эти маркеры являются необходимыми, но они ничего не гарантируют. Тем не менее, чем больше маркеров в своей предметной области вы обнаружите, тем выше вероятность, что Модель Акторов упростит вашу работу.
Принцип Fire-and-Forget
Первый маркер — это возможность использования принципа «послать-и-забыть».
О чем этот принцип?
Во-первых, он о том, что ход выполнения работ в вашей задаче не очень-то и нужно контролировать. Все само выполняется тогда, когда для этого находится подходящее время и ресурсы. Вы просто делаете свою часть, отдаете результаты своей работы куда-то дальше и вас больше не интересует, что с этими результатами произойдет.
Во-вторых, этот принцип о том, что в большинстве случаев вам не нужно знать результат начатой операции здесь и сейчас. Если вам что-то нужно, то вы отсылаете куда-то свою заявку и можете продолжать свою работу не дожидаясь результата обработки вашей заявки.
В-третьих, этот принцип о том, что если что-то вообще не будет сделано, то ничего страшного в этом нет. Мы либо можем проигнорировать отсутствие результата, либо же мы можем повторить операцию спустя какое-то время.
В общем-то это простой и очевидный принцип, которым мы регулярно пользуемся в повседневной жизни. В том числе и для решения сложных и важных задач.
Например, вы хотите провести конференцию для C++ разработчиков. Вам нужно пригласить интересных докладчиков. Вы составляете список тех, кого вам хотелось бы видеть и отсылаете им письма с вопросом о возможности участия. Письма ушли, вам не нужно ждать немедленных ответов. Пока люди думают, вы можете заниматься другими организационными вопросами. Если кто-то на ваш вопрос вообще не ответил, вы можете спросить его еще раз. Или же просто посчитаете, что человек не захотел принять участие и не будете на него рассчитывать.
Т.е. в реальной жизни мы часто применяем принцип «послать-и-забыть». Но из той же реальной жизни мы знаем, что он далеко не всегда работает. Собственно, тоже самое происходит и при написании программ. Где-то мы можем использовать «послать-и-забыть», а где-то — нет.
Например, у нас может быть две рабочих нити. На первой мы висим на select-е или epoll-е. Когда появляются данные для чтения, мы их вычитываем, отдаем второй рабочей нити для парсинга, а сами переходим к чтению данных из другого сокета. Для первой рабочей нити не важно, когда именно вторая рабочая нить выполнит парсинг и будет ли этот парсинг успешным или нет.
Другой пример. Мы коммитим транзакцию в базу данных. Скорее всего, нам сразу бы хотелось узнать результат: успешно ли прошел коммит или нет. И вряд ли мы можем продолжать работать пока результат коммита неизвестен.
В общем, если вы видите, что принцип «послать-и-забыть» для вашей задачи вполне естественен, то можно примерять на задачу Модель Акторов. А вот если вы видите, что вам практически всегда нужно сразу знать результаты начатых вами операций, то Модель Акторов вам вряд ли подойдет.
Конечные автоматы
Следующий важный маркер — это наличие в вашей предметной области сущностей, которые представимы в виде конечных автоматов.
Вообще, если посмотреть на принципы Модели Акторов, то можно увидеть, что акторы, по сути, это конечные автоматы, хоть и простые:
- актор – это некая сущность, обладающая поведением;
- акторы реагируют на входящие сообщения;
- получив сообщение актор может:
- отослать некоторое количество сообщений другим акторам;
- создать некоторое количество новых акторов;
- определить для себя новое поведение для обработки последующих сообщений.
Действительно, у каждого актора есть поведение, которое определяет, как актор будет обрабатывать следующее сообщение. В процессе обработки сообщения актор может выбрать для себя новое поведение.
Это ведь тоже самое, что и конечный автомат: у автомата есть его текущее состояние, которое определяет, как будет обработан входной сигнал. Новое состояние автомата определяется его текущим состоянием и типом пришедшего сигнала.
Поэтому неудивительно, что задачи, в которых широко используются конечные автоматы, хорошо ложатся на Модель Акторов.
Не все конечные автоматы одинаково полезны
Предположим, что у нас есть сущность, которая координирует процесс входа пользователя в систему, например, на сайт он-лайн кинотеатра. Эта сущность получает на вход запрос с именем пользователя и паролем, после чего запрашивает подсистему аутентификации. Если аутентификация прошла успешно, то делается запрос в подсистему биллинга, чтобы определить баланс пользователя. После чего делается запрос в подсистему нотификаций для получения списка уведомлений для пользователя. В результате формируется начальная страничка для пользователя, в которой отображается информация о его текущем балансе и список ожидающих реакции уведомлений.
Вроде бы все просто и понятно. Но что-то смущает.
А смущает то, что здесь конечный автомат не нужен вообще. По сути у нас простая линейная последовательность действий с синхронными обращениями к внешним подсистемам.
Для выражения такой последовательности гораздо лучше использовать обычные нити операционной системы. Чтобы каждое обращение к сторонней подсистеме было простым синхронным вызовом.
auto process_login(const login_params & params) -> start_page_data
{
const auto auth_result = request_auth_service(params);
if(auth_result.valid_user())
{
const auto balance = request_balance(auth_result.user_token());
const auto pending_messages = request_pending_messages(auth_result.user_token());
return make_start_page_data(auth_result, balance, pending_messages);
}
else
return make_unknown_user_page_data();
}
Конечно, тут есть проблемы с масштабируемостью, ведь создание отдельной нити для каждого пользователя — это слишком дорого. Но эта проблема решается, если у нас есть возможность применять файберы или сопрограммы. Тогда все действия оформляются в виде линейной сопрограммы с блокирующими вызовами внутри. И нам не нужны никакие конечные автоматы.
Соответственно, если большинство активностей в вашей программе представляются в виде линейных последовательностей из синхронных операций, то Модель Акторов вам вряд ли нужна. И смотреть вам нужно куда-то в сторону CSP или task-based parallelism-а.
Реально полезные конечные автоматы
А в каких же случаях конечные автоматы оказываются полезны?
Несколько типов входных сигналов в каждом из состояний
Во-первых, когда мы в каждый момент времени ждем не один тип входящего сигнала, а несколько.
Представьте себе, что нам нужно запрограммировать панель домофона. Панель активируется при первом нажатии на кнопку с цифрой. После чего мы можем ждать либо нажатие другой кнопки с цифрой, либо нажатие на кнопку «Вызов», либо нажатие кнопки «Сброс», чтобы сбросить введенный номер, но остаться в активированном состоянии, либо сигнал таймера о том, что пора деактивировать панель домофона.
Вот когда в каждом состоянии нам приходится реагировать на несколько разных типов сигналов, конечные автоматы могут оказаться более простыми и понятными в реализации, чем какой-либо другой подход.
Нелинейные переходы между состояниями
Во-вторых, конечные автоматы могут быть полезны, когда логика поведения оказывается нелинейной. Т.е. когда мы можем из состояния S1 перейти в состояние S2, оттуда в S3, а оттуда, в зависимости от входного сигнала мы можем вернуться либо в S1, либо в S2, а можем и перейти в S4, откуда мы можем вернуться в S2.
В случаях таких циклических переходов между состояниями конечный автомат также может оказаться удобнее попытки написать линейный код.
Продвинутые конечные автоматы
В-третьих, вам могут понадобиться продвинутые возможности конечных автоматов:
- реакция на вход/выход в/из состояния;
- иерархия состояний (вложенные состояния, наследование событий);
- история для состояний;
- ограничения на время пребывания в состоянии.
Все, что вы хотели знать про конечные автоматы, но...
Вообще, есть основополагающая статья на тему формальной нотации для диаграмм состояний от Девида Харела: Statecharts: A Visual Formalism For Complex Systems (1987).
Там разбираются различные ситуации, которые могут встретиться при работе с конечными автоматами на примере управления обычными электронными часами. Если кто-то не читал ее, то очень рекомендую. В принципе, все, что описывал Харел затем перешло в нотацию UML. Но когда читаешь описание диаграмм состояний из UML-я, то не всегда понимаешь что, для чего и когда нужно. А вот в статье Харела изложение идет от простых ситуаций к более сложным. И ты лучше осознаешь всю мощь, которую скрывают в себе конечные автоматы.
Очевидное резюме по конечным автоматам
Если ваша предметная область буквально кишит конечными автоматами, то вам прямая дорога в Модель Акторов.
Архитектура Shared Nothing
Следующий маркер, возможно, самый важный: насколько просто в вашей предметной области придерживаться принципа Shared Nothing.
Т.е. могут ли ваши сущности жить и работать вообще без разделяемых данных?
Если доводить до предела, то можно ли представить каждую вашу сущность в виде автономного процесса ОС, который общается с другими такими же процессами только через асинхронные сообщения?
В идеале акторы не должны иметь никаких общих данных. Каждый актор — это автономная самостоятельная сущность. Со своим собственным состоянием, которое больше никому не видно. Поэтому такое предельное представление, что каждый актор — это самостоятельный и независимый процесс с собственным адресным пространством очень даже оправдано.
Оно, правда, несколько экстремально. И мы можем от него отойти, например, по соображениям эффективности. Тем не менее, если мы в принципе можем представить себе решение, в котором каждый актор — это отдельный процесс, то это хороший знак.
Здесь нужно особо отметить два важных момента.
Shared Nothing не всегда возможен
Во-первых, очевидно, что далеко не всегда мы можем придерживаться принципа Shared Nothing.
Например, мы можем держать в памяти большой граф социальных связей. И для того, чтобы эффективно обрабатывать множество запросов к нему нам может потребоваться многопоточная обработка этих запросов. Рабочие потоки будут вынуждены совместно владеть графом и использовать какие-то механизмы синхронизации для того, чтобы не нарушить его целостность.
Другой пример: вычислительные задачи. Мы можем иметь в памяти несколько больших матриц, которые участвуют в проведении расчетов. Для ускорения расчета мы можем запускать несколько параллельных потоков и эти потоки будут совместно работать с общими данным.
Все зависит от высоты, с которой мы смотрим вниз...
Во-вторых, ситуация может меняться принципиально в зависимости от уровня абстракции, на котором вы рассматриваете свою задачу.
Давайте вернемся к примеру с он-лайн кинотеатром. Если смотреть «по большому сверху», то мы видим вполне себе архитектуру Shared Nothing. Подсистема аутентификации работает с собственными данными, биллинг работает с собственными данными, подсистема нотификаций работает с собственными данными. Разделять им всем нечего. Общаются друг с другом только посредством асинхронных сообщений. Т.е., по сути, все они являются акторами.
Однако, если мы опустимся на уровень реализации конкретного компонента, то там уже никаких автономных акторов может не быть в принципе.
Например, в подсистеме биллинга может быть огромная структура данных в оперативной памяти и несколько рабочих потоков, которые очень хитро с ней работают (например, с применением lock-free алгоритмов и персистентных структур данных).
Т.е. мы можем столкнуться с тем, что на концептуальном уровне у нас вроде как Модель Акторов есть, а на уровне реализации, в коде, у нас обычный многопоточный императивный треш, хардкор и содомия.
И это нормально. Еще раз вспомним, что Модель Акторов — это не просто набор приемов для написания кода. Это еще и подход к анализу предметной области и проектированию программной системы.
Поэтому мы можем использовать Модель Акторов на уровне проектирования, выделяя компоненты, которые концептуально являются акторами. Но вот на уровне реализации от Модели Акторов уже ничего не останется.
В общем-то, может быть и обратная ситуация: ваше приложение может быть огромным монолитом и вовсю использовать разделяемые между потоками данные, но в какой-то части этого приложения вы запросто можете использовать Модель Акторов, фактически изолируя часть приложения от остального кода.
Итого по поводу Shared Nothing
Если использование архитектуры Shared Nothing затруднительно и/или ведет к дополнительным накладным расходам, то в сторону Модели Акторов можно не смотреть.
Но вообще Shared Nothing — это отличная вещь. Очень сильно упрощает жизнь. Особенно в многопоточном программировании. И Модель Акторов способствует внедрению архитектуры Shared Nothing. Так что, если вы пытаетесь построить свою прикладную систему с использованием архитектуры Shared Nothing, то акторы могут вам в этом сильно помочь.
Таймеры
Отдельно стоит остановиться на работе с таймерами.
Нельзя сказать, что таймеры — это какой-то особый маркер, который присущ именно Модели Акторов. Но из-за принципа «послать-и-забыть» работа с таймерами оказывается очень важна. Бывает так, что мы запускаем какую-то операцию, а спустя некоторое время должны проверить ее результат. В этом случае без удобной работы с таймерами не обойтись.
В случае с акторами таймеры реализуются посредством отложенных сообщений. Что очень удобно, т.к. при срабатывании таймера вам приходит обычное сообщение.
Давайте рассмотрим простой пример:
Вы получаете запрос от пользователя. Но вы не хотите сразу обрабатывать его, потому что обработка одиночных запросов невыгодна. Вы можете подождать немного. Вдруг придет еще несколько запросов, тогда вы сможете обработать их все скопом. Например, вам выгодно обрабатывать запросы группами по 100 заявок. Это, конечно, ухудшает латентность для отдельного запроса, но зато улучшает пропускную способность вашего сервиса.
Получается, что вам нужно дождаться выполнения двух условий:
- Либо вы накопили 100 запросов и обработали их все сразу.
- Либо вы подождали, скажем, 250 миллисекунд и обрабатываете все, что успело поступить. 99 — значит 99. Один — значит один.
Реализуется это очень просто:
class bunch_processor {
...
public:
void on_request(request & req) {
requests_.push_back(move(req));
if(1 == requests_.size())
timeout_timer_ = send_delayed<timeout>(this, 250ms);
else if(100 == requests_.size()) {
timeout_timer_.reset();
handle_collected_requests();
}
}
void on_timer(timeout&) {
handle_collected_requests();
}
...
};
Резюмируя тему таймеров могу сказать, что акторы и таймеры очень хорошо дружат друг с другом. Поэтому если в вашей задаче много работы с таймерами, то Модель Акторов вам может помочь в этом.
Где же это все может применяться?
Ну и для того, чтобы закрепить материал, давайте попробуем бегло пройтись по областям, в которых, Модель Акторов зарекомендовала себя хорошо. То, что я буду говорить далее, основывается на моем собственном опыте и на опыте коллег, с которыми доводилось обсуждать тему Модели Акторов.
Управление оборудованием
Первая область, которая приходит в голову — это управление реальным оборудованием с помощью компьютера. Например, в задачах промышленной автоматизации.
Работа внешних устройств часто описывается с помощью конечных автоматов. Поэтому неудивительно, что для работы с устройствами в самой программе также используются конечные автоматы.
Взаимодействие между акторами посредством асинхронных сообщений также оказывается очень похоже на работу с реальным оборудованием. Поскольку общение с внешними устройствами часто выполняется именно асинхронно. Скажем, мы записываем команду в какой-то порт ввода-вывода. Затем должны подождать какое-то время, затем прочитать содержимое какого-то другого порта ввода-вывода, чтобы узнать выполнена наша команда или нет. Кстати говоря, удобная работа с таймерами в таких случаях очень помогает.
Имитационное моделирование
Еще одно направление — это имитационное моделирование каких-либо процессов, например, в системах массового обслуживания. Особенно процессов, включающих в себя множество разнообразных сущностей (см., например, Agent-Based Model).
Поскольку актор — это автономная сущность, обладающая собственным поведением, то с их помощью оказывается удобно моделировать объекты реального мира. Можно создавать совершенно разнотипных акторов, можно создавать однотипных акторов, которые отличаются только значениями некоторых параметров. Можно наполнить свою модель хоть миллионом акторов, каждый из которых хоть чем-то будет отличаться от остальных. И это позволяет проводить сложные эксперименты в области имитационного моделирования.
Разработка тестовых окружений
Когда разрабатываются компоненты больших программных систем бывает необходимо создать тестовое окружение, которое бы имитировало поведение смежных компонентов. Это бывает необходимо делать по разным причинам:
- самих смежных компонентов может еще не быть в наличии. Они разрабатываются параллельно и могут быть пока еще не готовы к совместному интеграционному тестированию. Поэтому вам нужен какой-то имитатор, способный заменить смежный компонент здесь и сейчас;
- вам может потребоваться имитация нештатного поведения смежного компонента. Например, вам нужно, чтобы смежный компонент сильно задерживал ответ на каждый 5-й запрос, вообще не отвечал на каждый 10-й запрос, а на каждый 20-й запрос отвечал каким-нибудь мусором.
Собственное тестовое окружение может потребоваться даже при разработке небольших систем, например, при работе с внешним оборудованием, когда у вас этого оборудования еще нет в наличии. Но вам нужен какой-то имитатор внешнего устройства.
Опыт показывает, что в таких случаях имитаторы на базе модели акторов реализуются легко и непринужденно. И это не случайно, поскольку здесь можно обнаружить много общего между работой с оборудованием и имитационным моделированием, о чем речь шла ранее.
Конвейерная обработка данных/транзакций
Конвейерная обработка потоков данных или потоков транзакций — это не совсем вотчина Модели Акторов. Это уже область data flow programming. Однако, на практике, Модель Акторов запросто может стать фундаментом, на котором строится конвейерная обработка. Так, стадии конвейера легко реализуются акторами, а передача информации от одной стадии к другой выполняется посредство асинхронных сообщений (в таких задачах хорошо себя чувствует принцип «послать-и-забыть»).
Большой плюс акторов в таких задачах в том, что акторы обладают состоянием и это позволяет им делать интересные вещи. Например, накапливать единичные запросы в пакеты чтобы далее выполнялась пакетная обработка. Такой пример мы уже рассматривали выше: актор получает первый запрос, взводит таймер и ждет либо пока сформируется полный пакет, либо пока сработает таймер.
Еще один удобный момент в том, что акторы могут перестраивать свои связи в динамике. Например, может быть актор, выполняющий балансировку нагрузки на пять подчиненных ему акторов-воркеров. Балансировщик может отслеживать за какое время каждый воркер обрабатывает очередной пакет. И если обнаруживает, что это время начинает расти, то балансировщик может снизить нагрузку на этого проблемного воркера.
Правда, если применять акторов в задачах конвейерной обработки данных, то всплывает проблема back pressure. Но это уже совсем другая история. Тем более, что она вполне себе решаема. И в том же Akka есть Akka Streams, которые построены именно поверх обычных Akka-акторов.
Несколько банальностей в завершение
Завершить же хочу в роли Капитана Очевидность, поэтому несколько банальностей:
- Нужно руководствоваться здравым смыслом при выборе подхода для решения своей задачи.
- Здравый смысл говорит, что Модель Акторов — не серебряная пуля.
- В ряде случаев Модель Акторов реально облегчает жизнь.
- Но для того, чтобы облегчить себе жизнь нужно иметь опыт работы с Моделью Акторов.
- Этот опыт лучше получать на игрушечных прототипах.
- Для того, чтобы получить такой опыт можно взять любой из имеющихся готовых акторных фреймворков для C++.