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, у обоих подходов сильные стороны превращаются в недостатки и наоборот.
В итоге для классического подхода считаю нужно выбирать 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, тогда самому как-то не комфортно «тыкать» уже немолодому человеку.
а в каждом деструкторе делается вызов 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 все-таки образуются.
А можно пояснить, как в случае №5 код соотносится с описанием «проблемы»? В конструкторе G4PhysicsModelCatalog создается статический объект типа modelCatalog, указатель на который сохраняется в статическом же члене класса. Такое впечатление, что разработчики хотели достичь того, чтобы экземпляр modelCatalog создавался бы только при создании первого экземпляра G4PhysicsModelCatalog. Если же G4PhysicsModelCatalog нет, то нет и экземпляра modelCatalog.
Ответ вам был дан. Почему вы не можете его прочитать и понять?
Я вас спрашиваю, какая реализация сортировки в общем случае используется в С?
Тогда позвольте вас спросить: мы будем под общий случай выдавать конкретную ситуацию? Тогда давайте посмотрим на такой вот «общий» случай:
void f(char * b, size_t i) { b[i] = 0; }
Значения b и i определяются в run-time, заранее они не известны.
Ну и, видимо, нужно более четко обозначить свою точку зрения, ибо очевидно, что для местных комментаторов она не очевидна:
— в Rust-е не так уж много особенностей, которые бы не позволили компилятору Rust-а сгенерировать такой же эффективный код, как и компилятору C. Одна особенность — это включенный по умолчанию bounds checking, вторая — это генерация таблиц для обработки panic (хотя это лишь косвенное влияние оказывает). А так, в принципе, у компилятора Rust-а достаточно информации, чтобы сгенерировать даже более оптимальный код;
— однако, производительность кода будет определяться не столько возможностями компилятора, сколько программистом. Rust является языком более высокого уровня, позволяет решать более сложные задачи, что дает возможность разработчику использовать более высокие уровни абстракции. Чем выше, тем меньше внимания уделяется тому, что внизу, откуда и проявляется некоторый проигрыш в производительности/ресурсоемкости по сравнению с более низкоуровневыми языками. Это не уникально для Rust-а, это уже проходилось, на моей памяти, как минимум с Ada, Eiffel и C++ (если говорить про то, что транслируется в нативный код);
— под общий случай лучше брать не какой-то микробенчмарк и уж тем более не часть микробенчмарка (вроде упомянутой вами сортировки), а решение какой-то большой задачи. Например, реализация MQ-шного брокера (хоть MQTT-брокера, хоть AMQP) или сервера СУБД. На таком объеме языки более высокого уровня сильно выигрывают в трудозатратах, качестве и надежности, но на какие-то проценты проигрывают в производительности тому же C (причины см. в предыдущем пункте). Если не верите, попробуйте пообщаться с разработчиками PostgreSQL или Tarantool-а.
Сейчас выглядит ну очень олдскульно и зачастую может быть заменена Boost-ом или POCO. Тем не менее, если знаний C++ недостаточно для разбирательства с потрохами Boost-а на какой-нибудь богом забытой платформе, то ACE вполне может быть достойным выбором и сегодня. ИМХО. конечно.
Тут мне лично не понятно, чьи именно это проблемы: отправителя или получателя.
Вряд ли получателя, поскольку если ему отсылает кто-то сообщение, в котором он не заинтересован, то сообщение просто теряется.
Ну а отправителю, повторюсь, не важно из-за чего именно его сообщение потеряно: не тот агент или где-то защита от перегрузки сработала.
По поводу контроля типов: ведь проблема того, что актор не обрабатывает сообщения типа A ничем не отличается от той проблемы, что актор-получатель по каким-то причинам решил не обрабатывать конкретное сообщение типа A. Т.е. пульнули сообщения актору, а дошло оно или нет, будет ли обработано, если дошло или не будет — вот это вот все фундаментальная особенность акторов. Понятно, что не везде это подходит. Но там другие подходы к concurrency просто используются.
Почитал. Первые две проблемы (с отсылкой Foo вместо Foo() и с подпиской актора на eventStream без его ведома) — это какая-то специфика Scala и Akka. Не стоит распространять эти аргументы на Модель Акторов вообще. В частности, у нас в SObjectizer и C++ подобные фокусы крайне сложно будет повторить.
Объективно, бывают случаи, когда хорошо было бы заранее знать, что отсылать сообщение типа x агенту А бесполезно, он такие сообщения в принципе не обрабатывает. Обычно такое бывает в результате разделения большого старого актора на несколько новых, поменьше. Раньше актор А обрабатывал x, теперь это делает актор B.
Но, опять же, это палка о двух концах. Т.к. со временем актор A вновь может начать обрабатывать x, только уже совсем по-другому.
Так что тут, имхо, нет однозначных преимуществ у проверок в compile-time и в run-time, у обоих подходов сильные стороны превращаются в недостатки и наоборот.
С SourceForge можно сразу бинарники для MSVC++ загрузить.
Для классического подхода к чему?
С Akka не работал, поэтому не сильно понимаю, как такое возможно.
Ну вот это как сильная, так и слабая сторона. Сильная сторона в том, что позволяет писать в стиле fire-and-forget, а так же позволяет легко подменять отправителей/получателей. С другой стороны, все проверки только в run-time. Но, опять же, за счет легкой подмены отправителей/получателей тестировать легко, а ведь тестировать саму обработку сообщения все равно нужно, даже если правильность подписки по типам гарантируется в compile-time.
Кроме того, это не столько проблема самой Actor Model, скорее отдельных ее реализаций. В CAF, например, делают каких-то типизированных акторов, у которых соответствие типов сигнала/слота проверяется в compile-time. Я, правда, не сильно понимаю, насколько это полезно на практике, мы как-то с подобными проблемами не сильно сталкивались. Да и с расширяемостью не очень понятно.
Плюс очень сильно жизнь упрощается, когда у фреймворка есть средства трассировки процесса доставки сообщений.
Об этом речь пойдет во второй части статьи.
Ну тут, скорее, смешение деталей реализации и опыта использования. В агенте должны быть какие-то атрибуты, которые нужны самому фреймворку для обслуживания агента. Удобно это оформлять в виде агента. Кроме того, практика показала, что в реальной жизни агенты очень быстро перерастают маленькие однострочники и могут достигать размеров в сотни, а то и в тысячи строк. Плюс временами активно используется наследование. Т.е. какой-то базовый класс агента вбирает в себя основной функционал, а другие классы-наследники только уточняют его работу. Так что использование здесь объектов и принципов ООП себя, на мой взгляд, оправдывает.
Кроме того, у нас есть такое понятие, как ad-hoc агент, когда агент просто конструируется из лямбда-функций без необходимости определять класс агента и создавать его экземпляр. Вот простой пример. А вот еще один — тут агент ponger реализуется в виде ad-hoc агента с лямбдой-однострочником.
Ну и дело вообще-то не в том, чтобы [[implies(std::nothrow)]] появился. А чтобы движение началось :)
Понятно, что байка. Но когда заглядываешь в исходники некоторых реализаций STL в нее начинаешь верить :)
Тем не менее, об STL-е нужно судить не по коду самого STL (там вполне ожидаемо буде хардкор, непонятный половине действующих C++ников), а по коду, который использует STL.
Если ко мне обращаются на «Вы», а мне с этим человеком приходится много общаться, то сам предлагаю перейти на «ты», чтобы было проще. Если только он не старше меня лет на 15-20, тогда самому как-то не комфортно «тыкать» уже немолодому человеку.
Ну если вы не увидели разницы между приведенными двумя примерами кода, значит ее нет, а я во всем неправ.
а в каждом деструкторе делается вызов close(), то код с деструкторами будет чуть-чуть дороже, чем код с прямым вызовом close():
Во-вторых, запись действий по очистке вручную может позволить записать действия более компактно. Т.е. если в коде на C++ между вызовами деструкторов a и b пройдет вызов еще нескольких деструкторов, то данные и код для вызова деструктора b уже могут уйти из кэша. Тогда как в C может быть записано что-то вроде:
Так что на практике выплывают некоторые мелочи из-за которых незначительное преимущества в скорости у C все-таки образуются.
Другое дело — стоят ли они того…
Ответ вам был дан. Почему вы не можете его прочитать и понять?
Тогда позвольте вас спросить: мы будем под общий случай выдавать конкретную ситуацию? Тогда давайте посмотрим на такой вот «общий» случай:
Значения b и i определяются в run-time, заранее они не известны.
Ну и, видимо, нужно более четко обозначить свою точку зрения, ибо очевидно, что для местных комментаторов она не очевидна:
— в Rust-е не так уж много особенностей, которые бы не позволили компилятору Rust-а сгенерировать такой же эффективный код, как и компилятору C. Одна особенность — это включенный по умолчанию bounds checking, вторая — это генерация таблиц для обработки panic (хотя это лишь косвенное влияние оказывает). А так, в принципе, у компилятора Rust-а достаточно информации, чтобы сгенерировать даже более оптимальный код;
— однако, производительность кода будет определяться не столько возможностями компилятора, сколько программистом. Rust является языком более высокого уровня, позволяет решать более сложные задачи, что дает возможность разработчику использовать более высокие уровни абстракции. Чем выше, тем меньше внимания уделяется тому, что внизу, откуда и проявляется некоторый проигрыш в производительности/ресурсоемкости по сравнению с более низкоуровневыми языками. Это не уникально для Rust-а, это уже проходилось, на моей памяти, как минимум с Ada, Eiffel и C++ (если говорить про то, что транслируется в нативный код);
— под общий случай лучше брать не какой-то микробенчмарк и уж тем более не часть микробенчмарка (вроде упомянутой вами сортировки), а решение какой-то большой задачи. Например, реализация MQ-шного брокера (хоть MQTT-брокера, хоть AMQP) или сервера СУБД. На таком объеме языки более высокого уровня сильно выигрывают в трудозатратах, качестве и надежности, но на какие-то проценты проигрывают в производительности тому же C (причины см. в предыдущем пункте). Если не верите, попробуйте пообщаться с разработчиками PostgreSQL или Tarantool-а.