вторник, 11 сентября 2018 г.

[prog.flame] Взгляд на Akka глазами разработчика SObjectizer-а

Некоторое время назад от читателя в комментариях поступила просьба сравнить SObjectizer и Akka. Попытался вдумчиво подойти к этому вопросу. Ибо тут было над чем подумать, т.к. на первый взгляд, сам факт такого сравнения достаточно странный. Как будто сравниваются апельсины с помидорами, просто на том основании, что и то, и другое можно съесть. Действительно, вряд ли у кого-то будет стоять выбор между C++ и SObjectizer-ом и Scala/Java и Akka. Скорее сперва выбирается язык/платформа, потом уже фреймворк для этого языка/платформы. Так что с точки зрения практики более уместным было бы сравнивать SObjectizer и CAF с QP/C++ в рамках C++. Или Akka и Vert.x в рамках JVM.

Тем не менее, поскольку и SObjectizer и Akka реализуют вроде как похожие идеи (активные самостоятельные сущности, взаимодействующие с окружением только посредством асинхронных сообщений), то было любопытно посмотреть, насколько по разному в SObjectizer и Akka подходят к реализации этих самых идей.

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

Под катом можно найти некоторые поверхностные соображения о том, в чем SObjectizer и Akka похожи, а в чем они сильно или не очень, но отличаются.

Принципиальные сходства и различия

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

В чем SO-5 и Akka принципиально похожи

Общее в SO-5 и в Akka проявляется в том, что в обоих фреймворках акторы/агенты представляются в виде объектов, у которых система исполнения (SObjectizer Run-Time или Akka ActorSystem) дергает коллбэки для обработки поступивших акторам сообщений.

Причем и там, и там прослеживаются другие важные схожести в архитектурных решениях:

  • акторы/агенты не имеют собственных контекстов исполнения, это находится в ведении диспетчеров. Диспетчеры выделяют рабочие контексты, на которых у акторов будут вызываться коллбэки для обработки сообщений. Задача программиста -- связать своих акторов с нужными диспетчерами;
  • очередями сообщений акторов владеют диспетчеры. В SO-5 это вообще очень жестко регламентируется и пользователь не может сильно повлиять на то, какую очередь диспетчер будет использовать для агента. В Akka большая гибкость, т.к. пользователь может повлиять на тип mailbox-а, но все равно конкретная реализация mailbox-а будет находится в распоряжении диспетчера;
  • сообщения и в SO-5, и в Akka представлены в виде экземпляров отдельных классов. Т.е. нужно тебе отослать какое-то сообщение -- определи для него тип, потом создай экземпляр этого типа и отсылай уже готовый экземпляр. Обработчик сообщения ищется по типу сообщения. Даже когда в качестве типа сообщения используется обычный String, этот принцип так же сохраняется.

Это сильно отличает SO-5 и Akka от других реализаций Модели Акторов, в которых акторы представляются обычными последовательными потоками исполнения (будь то легковесные процессы, как в Erlang, отдельные рабочие нити, как в Just::Thread Pro, или же короутины, как в Go или Vert.x).

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

В чем SO-5 и Akka принципиально различаются

Я вижу несколько важнейших различий между SO-5 и Akka, не рассмотреть которые никак нельзя. Т.к. без их понимания дальнейшее сравнение SO-5 и Akka теряет смысл. На мой взгляд, конечно же.

Реализация Модели Акторов vs результат эволюции небольшого инструмента

Пожалуй, самое главное и самое важное различие между Akka и SO-5 -- это то, что Akka является результатом разработки реализации конкретно Модели Акторов с изрядными заимствованиями основных подходов из Erlang-а. Отсюда, в частности, проистекает распределенность "из коробки", иерархия родитель-потомок и деревья супервизоров, рестарты акторов. Обо всем этом речь еще пойдет ниже, но акцент на этом нужно сделать прямо сейчас. Т.е. Akka -- это именно что реализация Модели Акторов, сделанная под влиянием Erlang-а.

Тогда как SO-5 -- это результат эволюции небольшого инструмента, который был предназначен для упрощения написания многопоточного кода. Поэтому в SO-5 может и не быть некоторой стройности концепций, которую можно увидеть в Akka. Так же в SO-5 могут быть видны артефакты эволюционного развития и какие-то рудименты от старых версий. Но, при этом, SO-5 дает программисту не только возможности Модели Акторов, но и возможности других подходов, вроде Pub/Sub или CSP-шные каналы.

При этом, наверняка, сказывается еще и разница в масштабах использования Akka и SO-5. Akka используется очень широко, поэтому разработчики Akka сталкиваются с большим разнообразием требований и пожеланий от пользователей. Удовлетворяя эти пожелания разработчики Akka сделали свой фреймворк более фичастым.

Ынтерпрайзное решение vs маленькая библиотека

При знакомстве с Akka складывается ощущение, что Akka -- это здоровенный продукт, предназначенный для сурового энтерпрайза. Особенно отчетливо это ощущаешь когда читаешь раздел про конфигуририрование Akka.

Тогда как SO-5 нами всегда рассматривался как небольшой инструмент, который может использоваться в проектах разного масштаба. Можно сделать SO-5 базой для всего приложения. А можно на SO-5 сделать только какую-то маленькую часть внутри, скажем, Qt-шного приложения.

Так же разница в масштабах ощущается когда разбираешься с тем, сколько функциональности и ручек для управления ею доступно "из коробки" в Akka и в SO-5. А так же когда видишь объем и структурированность документации по этим фреймворкам. Сразу чувствуется, что давно разрабатывается и эксплуатируется как широко известный коммерческий продукт, а что выросло на чистом энтузиазме из небольшой внутренней разработки.

Так что, по "глобальности и надежности" (с) Akka далеко впереди SObjectizer-а. Впрочем, это прямое следствие того, что по "глобальности и надежности" Java не менее впереди С++ :)

Распределенность vs локальность

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

В SO-5 поддержки распределенности из коробки нет. Задача SO-5 -- это эффективная диспетчеризация сообщений между сущностями в рамках одного процесса.

Отсутствие распределенности в SO-5 -- это не просто так. Это следствие того, что когда вы работаете в C++, то вопросы эффективности для вас не могут быть на 10-ом или каком-то еще более дальнем месте. И будет ли реализованный в акторном фреймворке коммуникационный протокол эффективным в вашей задаче -- это далеко не праздный вопрос. Поэтому в SO-5 мы сознательно отказались от поддержки распределенности.

Поэтому при программировании на SO-5 вы практически не будете сталкиваться с ситуациями, когда вам нужно будет понимать, на этой ли ноде ведется обработка или нет. Тогда как при использовании Akka о таких вещах вам может быть придется задумываться.

Одно из важнейший следствий принципиальных различий SO-5 и Akka: супервизоры и рестарты

Из описанных выше принципиальных различие вытекает одно архиважное следствие: в Akka иерархия супервизоров и рестарты акторов -- это один из краеугольных камней. На котором стоит как сам фреймворк Akka, так и построенные на основе Akka приложения.

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

И вот с этим-то перезапуском и связан, пожалуй, основной момент, на котором хочется заострить свое внимание.

Имеют ли смысл супервизоры в C++?

Тут, как будто, разные философии сталкиваются. С одной стороны, есть Erlang, безопасный язык, который делали под специфические условия. Из этих специфических условий можно выделить два. Первое -- это то, что наличие ошибок в программе признается очевидным фактом. С которым остается только смирится. А раз так, то падение процессов -- это неизбежность и с этой неизбежностью нужно как-то жить. Второе условие -- это то, что жить с падением процессов можно посредством рестартов. Ну вот такая предметная область была у автором Erlang-а: упал процесс, рестартовали его и все.

Причем авторов Erlang-а легко понять. Более того, они специально сделали у себя процессы. Изолированные. Со своими приватными данными. Как в нормальной ОС -- упал процесс и потерялось только то, что в нем было. Почистили за ним освободившиеся ресурсы и все, можно создавать новый процесс.

Перенос этого подхода в безопасные Scala/Java, хоть и вызывает некоторые вопросы (все-таки акторы -- это объекты внутри одного процесса, а не изолированные полностью процессы, поэтому актор все-таки может запортить какие-то глобальные разделяемые данные), но все-таки сохраняет смысл. Если ловить исключения, которые может выпустить актор при обработке сообщений, а потом рестартовать актора, то есть весьма высокие шансы, что приложение это переживет и ничего плохого с приложение не случится.

Тогда как в C++ ситуация сильно хуже. Если подходить к разработке на C++ так, что в коде все равно будут ошибки и приложение все равно будет падать, то далеко не уедешь. Поскольку C++ небезопасный язык, то у вас могут быть ошибки, которые, к сожалению, не приводят к мгновенному краху процесса. Ошиблись с адресной арифметикой, расстреляли чужую память и... И продолжили работать. Где-то, когда это скажется. Но именно что где-то и когда-то.

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

А раз так, то возникает закономерный вопрос: зачем тогда в C++ном фреймворке дублировать идеи супервизоров и настаивать на обязательном взаимоотношении родитель-потомок?

Вот я, например, удовлетворительного ответа на этот вопрос не нахожу.

Напрашивается очевидный ответ: чтобы было так, как и у других. Ну, ОК. Вполне себе подход и имеет право на существование. Проблема в том, что я с ним не согласен :) Если же кому-то важно иметь в C++ подобие Erlang-а с его деревьями супервизоров, то есть C++ Actor Framework, который копирует Erlang в C++ насколько, насколько это возможно.

Насколько удобен подход с иерархией родитель-потомок?

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

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

Наверное, с точки зрения красоты идеи, все это выглядит естественным. Не простыми, но естественным. А вот с практической точки зрения кооперация из SO-5 делает это вполне себе простым и очевидным действием. Поэтому мне думается, что в C++ SO-5 с кооперациями агентов -- это удобнее, чем иерархия из акторов в Akka. Но это речь про C++.

Кстати говоря, в SO-5 есть взаимоотношения родитель-потомок между кооперациями. Но возникли они не под воздействием идеи дерева супервизоров. А потому, что в C++ приходится следить за владением ресурсами и временами их жизни. Поэтому бывает удобно создать какой-то объект внутри агента родительской кооперации, а затем отдать обычные ссылки на этот объект агентам дочерних коопераций. А SObjectizer гарантирует, что сперва будут уничтожены агенты дочерних коопераций, затем уже агенты родительской кооперации. Из-за чего мы не будем беспокоится, что у дочерних агентов останутся повисшие ссылки на потроха родительского агента. И да, бывают сценарии, при которых умные указатели для достижения этого же эффекта -- это не выход.

Различия и, может быть, сходства на уровне реализации

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

Disclaimer. Поскольку я пишу нижеследующий текст только после беглого изучения документации, а не в результате опыта работы с Akka, то могу где-то ошибаться. Возможно, сильно. В этом случае прошу меня поправить.

Разные системы адресации

Поскольку Akka реализует Модель Акторов, то в Akka взаимодействие осуществляется между акторами посредством ссылок на сами акторы. Ссылка представляется в виде ActorRef. И для того, чтобы актор Alice мог отослать сообщение актору Bob, у Alice должен быть ActorRef, указывающий на Bob-а.

При этом, в принципе, ActorRef можно сериализовать и отослать по сети на другую ноду. Это возможно, поскольку в Akka распределенность встроенная.

Так же интересный момент в том, что в Akka за ActorRef может скрываться i-я инкарнация актора. Т.е. создали актора Alice, тот поработал какое-то время и "упал", его рестартовали. Получился новый экземпляр актора Alice. Но вот ActorRef, полученный при создании актора Alice, остался тем же самым. И другие акторы, которые знали про Alice только через ActorRef, могут даже понятия не иметь, что за ActorRef скрывается уже другой экземпляр. Пока актор рестартует его ActorRef можно использовать для отсылки новых сообщений.

Еще у акторов в Akka есть имена. По аналогии с файловой системой. Есть собственное имя актора (как имя файла в каталоге) и есть его полное имя (как полное имя файла, включая путь к нему). Так, если у актора собственное имя "child", а он является дочерним актору "parent" у которого нет своего родителя, то полное имя у "child"-а может быть "/user/parent/child", где "/user" -- это системный актор Akka, который является родителем для всех пользовательских акторов. Как я понимаю, полное имя актора может быть и еще сложнее, если это ссылка на актора на удаленной ноде.

В SObjectizer взаимодействие между агентами ведется через mbox-ы. Кто скрывается за mbox-ом -- в общем-то неизвестно. Если это multi-consumer mbox, то получить отосланное в mbox сообщение могут и 100 агентов, и вообще никто. В случае с single-consumer mbox-ами на другой стороне может быть не больше одного получателя. Но, в принципе, может не быть и никого.

В SObjectizer mbox-ы используются для различных трюков, например, для организации доставки сообщения группе получателей по схеме round-robin. В Akka такие трюки делаются с помощью механизма Router-ов. Т.е. для доставки сообщения используется ActorRef, за которым будет какой-то актор, у которого внутри обработка сообщений будет делегироваться экземпляру Router-а.

Некоторые особенности, связанные с сообщениями

В этом разделе я попробую пройтись по тем местам механизма доставки и обработки сообщений в Akka, которые мне показались интересными или необычными.

Sender для сообщений

В Akka у каждого сообщения есть sender (т.е. ссылка на отправителя сообщения). Поэтому при обработке сообщения можно отослать ответ этому самому sender-у. В SO-5 понятия отправителя нет. Если нужно, чтобы агент Alice на сообщение Req ответил сообщением Resp агенту Bob, то в сообщении Req нужно вручную передать mbox агента Bob.

Кстати говоря, из-за наличия sender-а у сообщения в Akka, механизм Send-And-Receive-Future, о котором речь пойдет ниже, сильно отличается от SObjectizer-овского request_future.

Гарантии и порядок доставки сообщений

И в Akka, и в SO-5 обеспечивается т.н. sender-FIFO. Т.е. сообщения, отосланные актору A одним отправителем, дойдут до актора A в том порядке, в котором они были отосланы. Тогда как между ними могут вклинится сообщения, отосланные другим отправителем.

Но с этим порядком в Akka есть нюанс. Как я понял, в Akka для актора создается несколько mailbox-ов: первый, обычный, используется для обычных сообщений. Второй же предназначен для системных сообщений, вроде сообщения Terminated, которое говорит о том, что дочерний актор завершил свою работу. Так вот, нет строгого FIFO для сообщений, попадающих в разные mailbox-ы актора.

Представьте себе, что есть родитель Alice и есть дочерний актор Bob. Bob отсылает Alice сообщения M1, M2 и M3, после чего завершает свою работу и Akka присылает Alice сообщение Terminated для Bob-а. Так вот, к Alice сообщение Terminated может прийти раньше, чем M1, M2 и M3.

В SO-5 каких нюансов нет.

Зато в SO-5 могут быть нюансы с потерей сообщений. В Akka актор не может просто так потерять сообщение, которое ему отправлено (правда, как я понимаю, это возможно, если используются BoundedMailbox-ы с ограничением на размер очереди). А вот в SO-5 сообщение может быть отвергнуто, например, фильтром доставки (delivery_filters) или же лимитом для сообщения (message_limits).

Необходимость обработки всех типов сообщений

Как я понял, в Akka актор должен обрабатывать сообщения всех типов, которые к нему приходят. Если какое-то сообщение не обработалось, то порождается исключение.

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

Разные типы mailbox-ов и приоритеты для сообщений

В Akka актору можно назначит разные типы mailbox-ов. В том числе и mailbox-ы, которые тем или иным образом сортируют сообщения. Например, mailbox типа UnboundedControlAwareMailbox пропускает вперед сообщения, которые наследуются от akka.dispatch.ControlMessage. А mailbox типа UnboundedPriorityMailbox сортирует сообщения по приоритетам, но при этом внутри одного приоритета порядок FIFO не сохраняется. Тогда как mailbox типа UnboundedStablePriorityMailbox и сортирует по приоритетам, и сохраняет FIFO внутри одного приоритета.

В SObjectizer ничего подобного нет. Приоритеты существуют только на уровне агентов. И для учета приоритетов агентов при диспетчеризации предназначены соответствующие диспетчеры (таковых сейчас три: prio_dedicated_threads::one_per_prio, prio_one_thread::quoted_round_robin и prio_one_thread::strictly_ordered).

Механизм Stash

В Akka можно унаследовать актора от класса AbstractActorWithStash и получить возможность откладывать те сообщения, которые актора сейчас не интересуют, "на потом". Как я понимаю, сообщения, которые сейчас актору не нужны, идут в некую отдельную временную очередь, содержимое которой затем возвращается в основной mailbox актора, когда актор вызывает become/unbecome (т.е. меняет свое состояние, об этом речь пойдет ниже).

В SO-5 такой возможности нет. В принципе нет. И даже если она кому-нибудь потребуется, сделать ее будет непросто.

Состояния актора

Под состоянием далее будет пониматься то, как агент выбирает обработку входящих сообщений. В одном состоянии он может обрабатывать один набор сообщений и одним образом, в другом состоянии -- другой набор сообщений и другим образом. В Модели Акторов синонимом "состоянию" является термин "поведение" (behaviour).

В SO-5 каждый агент -- это конечный автомат, у которого явным образом определяются состояния. Причем состояние в SO-5 -- это просто некий маркер. Фактически, это экземпляр типа so_5::state_t. Сколько агенту нужно состояний, столько экземпляров so_5::state_t нужно создать. При этом в SO-5 с состоянием агентов никакие данные не связаны. Все данные, которые нужны агенту, должны быть определены внутри класса агента. Если, скажем, агенту нужно строковое значение в состоянии st_one и массив целых чисел в состоянии st_two, то самый простой способ -- это объявить два атрибута в классе агента: первый типа std::string, второй -- std::vector<int>. Ну а если хочется экономить память, то можно включить их в состав std::variant.

В Akka существует два способа определить поведение/состояние актора. Первый способ, который доступен всем актором -- это использование методов become/unbecome. Т.е., если актору нужно сменить свое состояние, то он вызывает метод become и передает туда новый список обработчиков сообщений для актора.

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

А вот второй режим become хитрее. В этом режиме образуется стек состояний. Например, сперва был вызов become(b1, false). И текущим состоянием стало b1. Затем вызвали become(b2, false). И текущим стало поведение b2. Затем сделали вызов become(b3, false). И текущем стало состояние b3. Но состояния b1 и b2 никуда не делись. Они ушли в специальный стек. Который стал выглядеть как [b3, b2, b1]. Когда актор вызывает unbecome, то b3 из вершины стека выбрасывается и текущим состоянием оказывается b2. Если еще раз вызвать unbecome, то текущим состоянием станет b1.

Так что акторы в Akka уже есть что-то вроде конечного автомата. С не самой очевидной логикой, имхо, но это, наверное, дело вкуса и привычки.

Но этого мало. В Akka есть еще и модуль FSM, чтобы из актора можно было сделать настоящий конечный автомат. И тут, на мой взгляд, начинается настоящая жесть. По крайней мере в примерах кода на Java. Рекомендую сходить и посмотреть. В принципе, разобраться можно. Но Java-овский DSL для описания конечного автомата в Akka -- это не для слабонервных :) Вероятно, язык лучше не позволяет сделать. Но тем не менее.

Что в Akka делает работу с состояниями еще более суровой, так это то, что и при использовании become/unbecome, что при использовании FSM, для нового состояния создаются новые наборы данных, которые нужны актору в этом состоянии.

В документации в примерах к become это происходит за счет того, что нужные данные захватываются лямбдами, которые реализуют обработку сообщений в новом состоянии. Не то, чтобы я был в восторге, но с этим не очень сложно разобраться. А вот в случае FSM и Java начинает творится какой-то Ад и Израиль :)

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

Любопытное про таймеры

Оказывается, в Akka есть механизм ReceiveTimeout. Специальное уведомление приходит к актору, если его очередь сообщений пуста свыше какого-то времени. Например, нет ничего в очереди 100ms, прилетело уведомление. Не понятно, зачем это нужно, но прикольно. Надобности иметь что-то подобное в SO-5 у нас не было. Интересно, зачем это кому-то в Akka.

А вот что действительно полезно, так это то, что в Akka при отмене таймера отложенное сообщение до актора гарантированно не доходит. Даже если оно уже успело встать в очередь актора. В SO-5 можно отменить таймер, но если сообщение уже попало в очередь, то до агента оно дойдет. И в SO-5 от таких сообщений нужно защищаться вручную.

Диспетчеры

И в Akka, и в SObjectizer есть диспетчеры. Но в SObjectizer-е диспетчеры только обслуживают заявки агентов (т.е. только вызывают обработчики событий у агентов). А в Akka диспетчеры реализуют интерфейс ExecutionContext, что позволяет на диспетчерах запускать не только обработчики сообщений у акторов, но и произвольный код. Например, обработчики завершения операций на Future.

С типами диспетчеров в Akka я не очень понял. Вроде как есть всего три типа: Dispatcher, PinnedDispatcher и CallingThreadDispatcher. Но диспетчеры типов Dispatcher и PinnedDispatcher еще и могут иметь разные типы executor-ов. Например, fork-join-executor, thread-pool-executor, affinity-pool-executor и т.д. Так что, как я понял, в Akka нет большого выбора диспетчеров, но некоторые диспетчеры можно настраивать разными реализациями executor-ов. Что позволяет гибко настраивать контексты для акторов под конкретную задачу.

При этом в документации к Akka говорится буквально следующее:

It is important to realize that with the Actor Model you don't get any guarantee that the same thread will be executing the same actor for different messages.

Т.е. Akka со своими диспетчерами не дает явных гарантий того, что актор может быть привязан к конкретной рабочей нити (хотя неявно вы это себе можете обеспечить, если я правильно понимаю, за счет привязки актора к диспетчеру со thread-pool-executor-ом и всего одной рабочей нитью в пуле). В то время как в SO-5 механизм диспетчеров как раз и предназначен для того, чтобы пользователь мог привязать своих агентов так, как ему это нужно. К конкретной нити -- значит к конкретной. В SO-5 даже разрешено параллельно вызывать обработчики событий у агента на adv_thread_pool диспетчере.

В общем остается ощущение, что в SO-5 диспетчеры возведены в абсолют (являются одним из краеуголных каменей), тогда как в Akka диспетчер -- это одна из ручек настройки, которую можно подкрутить, если поведение по умолчанию по каким-то причинам не устраивает.

Механизм Ask (Send-And-Receive-Future)

В Akka есть механизм Ask (Send-And-Receive-Future), который похож на request_future в SO-5. Но т.к. в Akka используются более продвинутые Future, чем есть в стандартной библиотеке C++, то в Akka это выглядит более продвинутым. Пример оного можно посмотреть здесь.

При этом есть и принципиальное различие: в Akka агент, который получает запрос из ask, должен ответить обычным tell, т.е. сделать вызов вроде "getSender().tell(reply, getSelf())". И вот этот ответ уже Akka использует в соответствующей связке Promise/Future. Тогда как в SO-5 в качестве ответа забирается возвращаемое значение.

Еще один важный момент связанный с Send-And-Receive-Future: если в Akka при обработке запроса возникает исключение и это исключение нужно возвратить через Future, то обработчик запроса должен сделать это сам, вручную. Тогда как в SO-5 это происходит автоматически.

И еще один важный момент, который связан с Futures и диспетчерами. Когда Future срабатывает, то вызывается заданный пользователем коллбэк. Но этот callback вызывается не на контексте актора, который инициировал Ask. Поэтому в документации к Akka есть специальный абзац, предупреждающий о том, что из коллбэка не следует обращаться к содержимому актора, породившего Future. Т.к. и коллбэк, и актор могут в это время работать на разных нитях. В SO-5 такого нюанса нет в принципе.

Механизм deadLetter

Непонятный для меня механизм существует в Akka. Там все сообщения, для которых не смогли найти адресата, отправляются в отдельный специальный поток сообщений под названием deadLetter.

Вроде как эта фича используется для отладки приложений. Мол, если вы не понимаете, куда бесследно улетают ваши сообщения, то посмотрите в deadLetter. В SO-5 для подобных целей используется механизм message delivery tracing.

Средства для тестирования акторов

В Akka входит инструментарий для unit-тестирования акторов. И в документации по Akka этот инструментарий активно демонстрируется.

В принципе, это крутая штука. Поскольку unit-тестирование отдельных агентов может быть интересным квестом из-за асинхронной природы самих агентов. Но до такого инструмента в составе SO-5 мы, наверное, пока еще просто не доросли.

Заключение

Вот, пожалуй, и все, что удалось нарыть и сформулировать за то время, которое мы могли себе позволить выделить на такого рода работу. Еще раз приношу свои извинения за то, что получившееся сравнение выглядит как сплошная стена текста. Но, если бы я пытался разбавить ее хотя бы примерам кода, то данная работа заняла бы раза в три больше времени.

Мне было любопытно почитать документацию по Akka, ибо в последний раз я это делал довольно давно. И, по ощущениям, документация стала значительно лучше за прошедшее время. Не могу сказать, что для SO-5 у нас плохая документация, но до уровня документации Akka нам далеко. Тут есть над чем работать.

С удивлением узнал про некоторые вещи в Akka, о которых раньше не знал вообще. В частности, про механизм Routing и про то, что для акторов можно выбирать тип mailbox-а. Расширил свой кругозор, так сказать.

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

PS. Отдельный вопрос: нужен ли кому-нибудь данный текст в переводе на английский? Не знаю, на работе кому-нибудь показать, чтобы народ просвещался?

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