Как стать автором
Обновить
86
0.2
Евгений Охотников @eao197

Велосипедостроитель, программист-камикадзе

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

Статическая типизация никуда не девается. Вы вешаете обработчик на сообщение типа Msg1 и в этот обработчик приходит только сообщение типа Msg1. Сообщения других типов в ваш обработчик не попадут.

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

Все не так однозначно.

Во-первых, кому попало все равно можно отправить. Если вы отсылаете сообщения акторам A и B, и по ошибке отсылаете сообщение актору A вместо актора B, то вы все равно ошибаетесь с адресатом.

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

Так что в теории выгода от проверок в compile-time есть и она выглядит существенной. На практике все равно без тестирования прогона всех нужных сообщений не обойтись. Что уменьшает стоимость этого аргумента.
Adaptive Communication Environment — очень крутая для своего времени кросс-платформенная библиотека, абстрагирующая C++ разработчика от низкоуровневых деталей конкретных ОС.

Сейчас выглядит ну очень олдскульно и зачастую может быть заменена Boost-ом или POCO. Тем не менее, если знаний C++ недостаточно для разбирательства с потрохами Boost-а на какой-нибудь богом забытой платформе, то ACE вполне может быть достойным выбором и сегодня. ИМХО. конечно.
Все таки я вижу отличие в одном случае это часть логики — т.е. мы так задумали что сообщение не обрабатывается, в другом это не ожидаемо(не корректное поведение).

Тут мне лично не понятно, чьи именно это проблемы: отправителя или получателя.
Вряд ли получателя, поскольку если ему отсылает кто-то сообщение, в котором он не заинтересован, то сообщение просто теряется.

Ну а отправителю, повторюсь, не важно из-за чего именно его сообщение потеряно: не тот агент или где-то защита от перегрузки сработала.
Подробности забылись где-то в районе 2004-2005 годов, когда мы заменяли свои древние, самописные велосипеды (которые из которых росли еще со времен OS/2) на ACE. Основная причина была в том, что глупо было тянуть собственные разработки и заниматься их багфиксингом и модернизацией, когда все готовое уже было в ACE. Включая и средства для работы с DLL.
Да вроде вы высказались достаточно определенно. Просто если у вас опыт с Akka, то вот этот итог: "Сейчас вижу только одно применение акторам", он же не объективен. У людей, которые работали с другими реализациями Модели Акторов, итоговые впечатления могут сильно отличаться.

По поводу контроля типов: ведь проблема того, что актор не обрабатывает сообщения типа A ничем не отличается от той проблемы, что актор-получатель по каким-то причинам решил не обрабатывать конкретное сообщение типа A. Т.е. пульнули сообщения актору, а дошло оно или нет, будет ли обработано, если дошло или не будет — вот это вот все фундаментальная особенность акторов. Понятно, что не везде это подходит. Но там другие подходы к concurrency просто используются.
В целом хорошо описано тут: http://stew.vireo.org/posts/I-hate-akka/

Почитал. Первые две проблемы (с отсылкой Foo вместо Foo() и с подпиской актора на eventStream без его ведома) — это какая-то специфика Scala и Akka. Не стоит распространять эти аргументы на Модель Акторов вообще. В частности, у нас в SObjectizer и C++ подобные фокусы крайне сложно будет повторить.
А как вы планируете типами гарантировать, что актор послал сообщения тем кому «должен» послать и от кого «должен» принять?

Объективно, бывают случаи, когда хорошо было бы заранее знать, что отсылать сообщение типа x агенту А бесполезно, он такие сообщения в принципе не обрабатывает. Обычно такое бывает в результате разделения большого старого актора на несколько новых, поменьше. Раньше актор А обрабатывал x, теперь это делает актор B.

Но, опять же, это палка о двух концах. Т.к. со временем актор A вновь может начать обрабатывать x, только уже совсем по-другому.

Так что тут, имхо, нет однозначных преимуществ у проверок в compile-time и в run-time, у обоих подходов сильные стороны превращаются в недостатки и наоборот.
Не уверен, что понял ваш вопрос. Вы хотите видеть примеры прикладных систем, которые разработаны с использованием Модели Акторов?
Конечно. Берете SObjectizer с SourceForge или с github-а и пробуете сколько угодно :)
С SourceForge можно сразу бинарники для MSVC++ загрузить.
Упс, ошибся с веткой для ответа. Ответил вам здесь.
В итоге для классического подхода считаю нужно выбирать Future, далее объясню что имею ввиду.

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

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

Ну вот это как сильная, так и слабая сторона. Сильная сторона в том, что позволяет писать в стиле fire-and-forget, а так же позволяет легко подменять отправителей/получателей. С другой стороны, все проверки только в run-time. Но, опять же, за счет легкой подмены отправителей/получателей тестировать легко, а ведь тестировать саму обработку сообщения все равно нужно, даже если правильность подписки по типам гарантируется в compile-time.

Кроме того, это не столько проблема самой Actor Model, скорее отдельных ее реализаций. В CAF, например, делают каких-то типизированных акторов, у которых соответствие типов сигнала/слота проверяется в compile-time. Я, правда, не сильно понимаю, насколько это полезно на практике, мы как-то с подобными проблемами не сильно сталкивались. Да и с расширяемостью не очень понятно.

Плюс очень сильно жизнь упрощается, когда у фреймворка есть средства трассировки процесса доставки сообщений.
построение stateful вместо stateless приложений когда на каждую сущность в системе ровно один актор

Об этом речь пойдет во второй части статьи.
Пока, у меня возник, только, один вопрос: а почему агенты обязаны быть объектами — экземплярами классов C++?

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

Кроме того, у нас есть такое понятие, как ad-hoc агент, когда агент просто конструируется из лямбда-функций без необходимости определять класс агента и создавать его экземпляр. Вот простой пример. А вот еще один — тут агент ponger реализуется в виде ad-hoc агента с лямбдой-однострочником.
Контракты я использовал в Eiffel-е и, частично, в D. Во-первых, это не совсем про то. Контрактами, например, нельзя показать, что функция чистая. Или что она нерекурсивная. В том же D ввели отдельный атрибут pure, который к контрактам не относится. Во-вторых, что было для меня неожиданностью: Eiffel располагает к использованию контрактов, там это происходит само собой, естественным образом. А в D контракты хоть и были, но нужно было заставлять себя придерживаться дисциплины и описывать их.

Ну и дело вообще-то не в том, чтобы [[implies(std::nothrow)]] появился. А чтобы движение началось :)
В середине 90-х слышал байку, что реализацию STL для MS VC++ писал большой любитель Lisp-а и делал он это на каком-то вполне себе читабельном DSL-е. Из которого уже генерировалось вот этот вот все непотребство с нечитабельными именами. Причем нечитабельные имена были выбраны специально. Вроде как даже несколько причин было для этого: начиная от того, что первые реализации STL должны были работать даже на компиляторах без поддержки namespaces, и заканчивая тем, чтобы не возникало конфликтов имен у пользователей, если кто-то отнаследуется от std::vector и захочет добавить парочку своих атрибутов в класс-наследник.

Понятно, что байка. Но когда заглядываешь в исходники некоторых реализаций STL в нее начинаешь верить :)

Тем не менее, об STL-е нужно судить не по коду самого STL (там вполне ожидаемо буде хардкор, непонятный половине действующих C++ников), а по коду, который использует STL.
Есть вот такая смутная хотелка. Пока что сумбур и, очевидно, что мне этого просто не поднять. Но вроде как подобные вещи интересны и нужны не только мне. Так что может кто-нибудь заинтересуется и загорится. Ну или просто подтвердит, что направление перспективное.
По поводу плагинов в виде .dll/.so: приходилось использовать и очень активно. Вначале использовались свои обертки вокруг «родного» API OS, затем перешли на работу посредством ACE.
Зависит от того, как человек представляется (или как его представляют). Если представляют по имени-отчеству, то обращаюсь на «Вы», до тех пор, пока он сам не предложит перейти на «ты».
Если ко мне обращаются на «Вы», а мне с этим человеком приходится много общаться, то сам предлагаю перейти на «ты», чтобы было проще. Если только он не старше меня лет на 15-20, тогда самому как-то не комфортно «тыкать» уже немолодому человеку.
Если они не инлайнятся — значит, компилятор считает, что так будет быстрее и вполне возможно, что он прав.
Или в принципе не может это сделать, т.к. они лежат в отдельной dll-е.
Какая разница, как это записать?
Ну если вы не увидели разницы между приведенными двумя примерами кода, значит ее нет, а я во всем неправ.
Не так все просто. Во-первых, в том же C++ деструкторы не всегда инлайнятся. Т.е. если где-то в коде написано что-то вроде:
void f() {
  File a(...);
  ...
  File b(...);
  ...
} // (1)

а в каждом деструкторе делается вызов close(), то код с деструкторами будет чуть-чуть дороже, чем код с прямым вызовом close():
void f() {
  int a = open(...);
  ...
  int b = open(...);
  ...
  close(b);
  close(a);
}

Во-вторых, запись действий по очистке вручную может позволить записать действия более компактно. Т.е. если в коде на C++ между вызовами деструкторов a и b пройдет вызов еще нескольких деструкторов, то данные и код для вызова деструктора b уже могут уйти из кэша. Тогда как в C может быть записано что-то вроде:
void f() {
  int a, b;
  ...
  a = open(...);
  ...
  b = open(...);
  ...
cleanup:
  ... // Какие-то другие действия.
  close(a);
  close(b);
}

Так что на практике выплывают некоторые мелочи из-за которых незначительное преимущества в скорости у C все-таки образуются.

Другое дело — стоят ли они того…

Информация

В рейтинге
2 473-й
Откуда
Гомель, Гомельская обл., Беларусь
Зарегистрирован
Активность