LINUX.ORG.RU

SObjectizer-5.6.0: новая мажорная версия акторного фреймворка для C++

 , , ,


0

2

SObjectizer — это относительно небольшой фреймворк для упрощения разработки сложных многопоточных приложений на C++. SObjectizer позволяет разработчику строить свои программы на базе асинхронного обмена сообщениями с использованием таких подходов, как Actor Model, Publish-Subscribe и CSP. Это открытый проект под лицензией BSD-3-CLAUSE. Краткое впечатление о SObjectizer можно составить на основании вот этой презентации.

Версия 5.6.0 является первым мажорным релизом новой ветки SObjectizer-5.6. Что означает также завершение развития ветки SObjectizer-5.5, которая развивалась более четырех лет.

Поскольку версия 5.6.0 открывает новую главу развития SObjectizer, то нововведений совсем нет в сравнении с тем, что было изменено и/или удалено из SObjectizer. В частности:

  • используется C++17 (ранее обходились подмножеством C++11);
  • проект переехал и живет теперь на BitBucket с официальным, а не экспериментальным, зеркалом на GitHub;
  • у коопераций агентов нет больше строковых имен;
  • из SObjectizer удалена поддержка синхронного взаимодействия между агентами (его аналог реализован в сопутствующем проекте so5extra);
  • удалена поддержка агентов ad-hoc;
  • для отсылки сообщений теперь используются только свободные функции send, send_delayed, send_periodic (старые методы deliver_message, schedule_timer, single_timer из публичного API изъяты);
  • функции send_delayed и send_periodic теперь имеют единый формат вне зависимости от типа получателя сообщения (будь то mbox, mchain или ссылка на агента);
  • добавлен класс message_holder_t для упрощения работы с преаллоцированными сообщениями;
  • удалено множество вещей, которые были помечены как deprecated еще в ветке 5.5;
  • ну и еще всякое разное.

Более развернутый список изменений можно найти тут. Там же, в Wiki проекта, можно найти документацию по версии 5.6.

Архивы с новой версией SObjectizer можно взять на BitBucket или на SourceForge.

PS. Специально для скептиков, которые считают, что SObjectizer никому не нужен и никем не используется. Это не так.

>>> Подробности

★★★★★

Проверено: Shaman007 ()
Последнее исправление: Virtuos86 (всего исправлений: 3)

Ответ на: комментарий от rGlory

Разработка приложений на основе асинхронных сообщений усложняет разработку на порядок, а то и на несколько порядков.

Как минимум, пользователи Erlang/Elixir и Akka, а так же разработчики на Go, с вами сильно не согласятся.

eao197 ★★★★★
() автор топика
Ответ на: комментарий от eao197

Как минимум, пользователи Erlang/Elixir и Akka, а так же разработчики на Go, с вами сильно не согласятся.

Честно говоря, я не в курсе как там у них в Erlang и Akka, но какое это имеет отношение к разработке в С++? Разработка на основе событий имеет место быть и бывает оправдана по разным причинам. Но имхо - упрощение процесса разработки не одна из этих причин ни разу.

rGlory
()
Ответ на: комментарий от rGlory

Честно говоря, я не в курсе как там у них в Erlang и Akka

Ясно-понятно.

но какое это имеет отношение к разработке в С++?

Самое прямое. Собственно, я об этом уже подробно рассказывал, нет смысла повторяться.

Но имхо - упрощение процесса разработки не одна из этих причин ни разу.

С таким имхо разве поспоришь?

А вот в новости ссылка на опыт использования была, там человек пишет, что упрощает.

eao197 ★★★★★
() автор топика
Ответ на: комментарий от eao197

но какое это имеет отношение к разработке в С++?

Самое прямое. Собственно, я об этом уже подробно рассказывал, нет смысла повторятьс

Ну и вот здесь еще на эту же тему.

eao197 ★★★★★
() автор топика
Ответ на: комментарий от eao197

А вот в новости ссылка на опыт использования была, там человек пишет, что упрощает.

Целый один пишет? Ну тогда это все меняет. Несомненно можно найти (специально подобранную) задачу, где с асинхронным программированием все становится легко и просто. Однако в реальной жизни не все так просто и гладко, к сожалению. Достаточно сравнить размер и сложность кода при такой простой задаче, как запрос и получение данных с сервера базы данных. С предварительным логином и восстановлением соединения если нужно. Обычный синхронный код будет тривиальным, асинхронный будет довольно сложен для понимания - отслеживание состояний и прочая.

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

rGlory
()
Ответ на: комментарий от rGlory

Асинхронный код, однозначно сложнее синхронного. Но тут речь о другом. Если тебе нужна многопоточка, то без асинхронщины и сообщений не обойтись. И тут быстро становится понятно, что мьютексы и условные переменные подходят для решения только простых задач. Для более сложных нужны другие инструменты. SO один из таких, т.е. с ним проще разрабатывать многопоточный код, чем без него. Разумеется, он подходит не для любой задачи.

anonymous
()
Ответ на: комментарий от rGlory

Целый один пишет?

Целый один потрудился взять и описать свой опыт.

Однако в реальной жизни не все так просто и гладко, к сожалению.

Не все. Так никто и не обещал, что SObjectizer облегчает написание любых многопоточных приложений. Некоторых упрощает и сильно. Ссылку на статью, где описаны критерии для выбора, я уже давал выше. Если вам лень читать, то почему мне не должно быть лень вам что-то объяснять?

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

А теперь давайте представим, что у вас таких параллельных запросов 500K.

А теперь попробуйте представить, как эти 500K запросов должен обрабатывать этот самый сервер БД. Ему ведь нужно запросы принимать, парсить, строить планы, работать с кэшем и диском и т.д. и т.п.

Думаете, все это будет легко сделать на обычных нитях, мутексах и кондишенах? И что очереди сообщений здесь никак не помогут?

Ну ОК, не буду вас разубеждать.

eao197 ★★★★★
() автор топика
Последнее исправление: eao197 (всего исправлений: 1)
Ответ на: комментарий от eao197

А теперь попробуйте представить, как эти 500K запросов должен обрабатывать этот самый сервер БД.

Ога, 500000 параллельных запросов на сервере БД. Лол. Много знаешь таких СУБД? Научи коннектиться к таким серверам из параллельной вселенной, фантазер.

anonymous
()
Ответ на: комментарий от anonymous

Много знаешь таких СУБД?

Нет. А вам нужно обязательно много?

Насколько я слышал, 500K параллельных запросов для DynamoDB – это обыденность.

Ну и под «сервер БД» не обязательно понимать один-единственный физический сервер.

eao197 ★★★★★
() автор топика
Последнее исправление: eao197 (всего исправлений: 1)
Ответ на: комментарий от eao197

Насколько я слышал, 500K параллельных запросов для DynamoDB – это обыденность.

Ну вот теперь то пользователи ЛОРа знают какие инструменты использовать для следующего проекта: C++, SObjectizer и Amazom DynamoDB чтобы обработать 500000 параллельных запросов в секунду.

anonymous
()
Ответ на: комментарий от eao197

Думаете, все это будет легко сделать на обычных нитях, мутексах и кондишенах? И что очереди сообщений здесь никак не помогут?

Написать сервер, который сможет обрабатывать 500K запросов непросто полюбому. Прикол в том, что я могу реализовать очереди сообщений на обычных мьютексах и кондишенах, и даже прицепить «lock free». Но использовать очереди сообщений и сделать код полностью асинхронным - это две большие разницы. К сожалению эта модель плохо уживается с обычным, блокируемым кодом.

rGlory
()
Ответ на: комментарий от rGlory

Прикол в том, что я могу реализовать очереди сообщений на обычных мьютексах и кондишенах

Прикол в том, что вы можете взять готовую реализацию. Тот же SObjectizer, например.

Но использовать очереди сообщений и сделать код полностью асинхронным - это две большие разницы.

Если вас SObjectizer заставляет делать код полностью асинхронным, то SObjectizer вам не подойдет. В этом можете не сомневаться.

eao197 ★★★★★
() автор топика
Ответ на: комментарий от eao197

Если вас SObjectizer заставляет делать код полностью асинхронным, то SObjectizer вам не подойдет. В этом можете не сомневаться.

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

Прикол в том, что вы можете взять готовую реализацию. Тот же SObjectizer, например.

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

rGlory
()
Ответ на: комментарий от rGlory

Не меня, а всех.

И что вам позволяет говорить за всех?

Сама модель один объект - одна нить

Откуда взялась эта модель?

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

Я понимаю, что вы ничего не хотите знать про SObjectizer. Но, пилять, текст новости, в обсуждение которой вы решили вступить, вы прочитать можете? Упоминание Pub/Sub и CSP там в состоянии рассмотреть?

Может просто чукча не читатель?

А смысл?

Экономия сил и времени, т.к. SObjectizer – это не только очереди.

Да и возможно ли это, использовать SObjectizer без перехода на полностью асинхронную модель?

Можно. Здесь есть примеры. На асинхронных сообщениях только часть логики.

eao197 ★★★★★
() автор топика
Последнее исправление: eao197 (всего исправлений: 1)
Ответ на: комментарий от eao197

Откуда взялась эта модель?

Это ваша статья, которую тут рекомендовали? https://habr.com/ru/post/322250/ Это цитата из вашей статьи:

Вместо того, чтобы иметь N потоков, которые конкурируют друг с другом за ресурсы, можно сделать M потоков, каждый из которых будет владеть собственными данными. Ни один из потоков не имеет доступа к данным других потоков.

А вот в чем разница, вы говорите про данные, а я говорю про объект. Но это разве не одно и тоже?

Может просто чукча не читатель?

Может чукча прочитает свою статью?

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

Это что не подразумевает, что поток Y тоже становится асинхронным? И так далее по списку. Чем это отличается от того, что я сказал ранее?

rGlory
()
Последнее исправление: rGlory (всего исправлений: 1)
Ответ на: комментарий от rGlory

Это ваша статья, которую тут рекомендовали?

Моя.

А вот в чем разница, вы говорите про данные, а я говорю про объект. Но это разве не одно и тоже?

Один поток, вообще-то говоря, может обслуживать несколько объектов. Это актуально и для SObjctizer-а, и для реализаций stackful короутин на базе файберов, и для реализаций гороутин в Go.

Так что взаимоотношение один поток = один объект — это актуально для отдельных реализаций модели акторов (вроде Erlang-а), но не для всех.

Это что не подразумевает, что поток Y тоже становится асинхронным?

С чего бы это? Между потоками X и Y взаимодействие происходит посредством асинхронного обмена сообщениями. Т.е. когда X отправляет сообщение Y, то Y не обязан сидеть и ждать это сообщение.

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

Чем это отличается от того, что я сказал ранее?

Простите, что именно из того, что вы сказали ранее? Тут уже много вы наговорили.

eao197 ★★★★★
() автор топика
Ответ на: комментарий от eao197

Один поток, вообще-то говоря, может обслуживать несколько объектов.

Это вопрос терминологии. Принципиально тут то, что поток имеет экслюзивный набор данных. Я весь этот набор называю объектом, вы нет, суть от этого не меняется.

С чего бы это? Между потоками X и Y взаимодействие происходит посредством асинхронного обмена сообщениями. Т.е. когда X отправляет сообщение Y, то Y не обязан сидеть и ждать это сообщение.

С того, что если Y взаимодействует с X посредством «асинхронного обмена сообщениями» это налагает требования на Y

1 - Y должен уметь обрабатывать асинхронные сообщения. 2 - Y не должен блокироваться при обработке сообщений.

Из 2 следует, что Y должен общаться с другими посредством «асинхронного обмена сообщениями». А из 1 и 2 следует, что Y должен быть таким же как X - асинхронно обрабатывать. И так далее по списку.

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

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

rGlory
()
Ответ на: комментарий от Kuzy

Что такое синхронный и асинхронный по-твоему?

Грубо говоря вот синхронный:

   try {
      data = object.getData();
   }
   catch( const std::exception &ex ) {
       ...
   }

вот асинхронный

switch( event->type ) {
    case DataArrived: data = event->data; break;
      state = idle;
    case Timeout : // error message
      state = error;
}
rGlory
()
Последнее исправление: rGlory (всего исправлений: 1)
Ответ на: комментарий от rGlory

Это вопрос терминологии.

Нет. Если один рабочий поток может обслуживать несколько объектов, то объекты вынуждены поддерживать кооперативную многозадачность.

1 - Y должен уметь обрабатывать асинхронные сообщения. 2 - Y не должен блокироваться при обработке сообщений.

Ничего подобного. Асинхронность важна для X: он не должен блокироваться на операции передачи сообщения Y. Будет ли блокироваться Y на получении и обработки сообщения не суть важно.

eao197 ★★★★★
() автор топика
Ответ на: комментарий от eao197

Будет ли блокироваться Y на получении и обработки сообщения не суть важно.

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

rGlory
()
Последнее исправление: rGlory (всего исправлений: 1)
Ответ на: комментарий от rGlory

Повторяю еще раз: асинхронность означает, что X, которому что-то нужно от Y, не блокируется при доставке сообщения Y. В примере выше вы как раз показали что происходит на стороне X, а не Y.

Пока вы это не поймете, вы и будете нести чушь в духе «Если Y блокируется в итоге мы получаем однопоточную систему с гемором в виде ассинхронных сообщений.»

eao197 ★★★★★
() автор топика
Ответ на: комментарий от eao197

Пока вы это не поймете, вы и будете нести чушь

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

rGlory
()
Ответ на: комментарий от rGlory

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

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

В SObjectizer'е потоки, на которых работают объекты (они же агенты в терминах SO) управляются диспетчерами. Есть несколько видов диспетчеров: один поток для каждого объекта (агента), несколько объектов на один поток (вот тут то и нужна кооперативная многозадачность), несколько объектов на пул потоков (вот это и может быть решением ситуации, когда поток Y не справляется со всеми агентами), а также другие диспетчеры. Каждый агент, даже если привязан к диспетчеру с пулом потоков может в один момент времени работать только на одном потоке (т.е. работает только один экземпляр объекта). Кроме суперагентов, которые могут работать параллельно, обрабатывая разные сообщения на разных потоках (также может помочь в случае, когда поток Y не успевает, например, назначить этому объекту-агенту диспетчер с пулом потоков и разрешить его выполнять параллельно, если такое возможно).

Также в SO есть несколько (довольно примитивных, потому что без знания специфики задачи для общего случая ничего лучше не придумаешь) способов контроля перегруженности агентов (когда очередь растет быстрее, чем агент ее разгребает). Разумеется можно реализовать обратную связь и вручную, понимая характер обмена данными в своем приложении, но это уже вручную.

Так что задачу, когда поток Y тормозит все приложение, потому что загружен на 100% можно попробовать решить разными способами (часть из них встроена в SObjectizer). Но если эта ситуация типа нам надо писать данные в базу в одно подключение, а база не успевает, то тут никакая архитектура приложения не поможет. Потому что скорость обработки информации ограничивается скоростью работы базы. И да, приложение станет однопоточным с кучей асинхронщины, которая еще и защищена мьютексами при каждом обращении к очереди сообщений каждого агента.

anonymous
()
Ответ на: комментарий от rGlory

что я понимаю больше чем вам кажется

Для этого нужны, по меньшей мере, какие-то понятные и последовательные комментарии с вашей стороны.

Еще раз повторю: Y должен обрабатывать сообщения быстро, иначе вся система превратится в усложненную однопоточную программу.

Вообще-то до сих пор вы говорили о блокировках потока Y. Давайте я вам расскажу, что понимается под блокировками.

Блокировка – это когда поток приостанавливается до наступления какого-то внешнего события и ничего не делает пока это событие не наступит.

Примитивный пример для иллюстрации:

class ProtectedSharedData {
   std::mutex lock_;
   ...
public:
   void f() {
      std::lock_guard l{lock_};
      ...
   }
   void g() {
      std::lock_guard l{lock_};
      ...
   }
}

void threadX(ProtectedSharedData & d) {
   d.f();
}

void threadY(ProtectedSharedData & d) {
   d.g();
}

Вот здесь нить threadX, если она стартовала раньше, захватывает мутекс объекта d. И, когда тоже самое пытается сделать threadY, то threadY блокируется на вызове d. Т.е. она приостановлена и ничего (т.е. вообще ничего) не может делать пока не произойдет внешнее событие – threadX выйдет из вызова d.f().

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

class ThreadSafeQueue {
   std::mutex lock_;
   std::condition_variable cv_;
   std::deque<Message *> queue_;
   ...
public :
   void push(Message * m) {
      std::lock_guard l{lock_};
      queue_.push_back(m);
      ...
   }

   template<typename Lambda>
   void handle(Lambda && l) {
      std::unique_lock l{lock_};
      ... // Проверка пустоты и ожидания на пустой очереди.
      Message * m = queue_.front();
      queue_.pop_front();
      // Внимание! Вызов обработчика при захваченном lock_.
      l(m);
    }
}

void threadX(ThreadSafeQueue & q) {
   while(true) {
      q.push(...);
      ...
   }
}

void threadY(ThreadSafeQueue & q) {
   while(true) {
      ...
      q.handle([&](Message * m) {...});
   }
}

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

Обсуждения вот таких вещей ожидаешь, когда вы говорите про блокировки потока.

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

eao197 ★★★★★
() автор топика
Ответ на: комментарий от rGlory

Y должен обрабатывать сообщения быстро, иначе вся система превратится в усложненную однопоточную программу.

Так вот о проблеме producer-consumer. Эта проблема возникает, когда producer может сгенерировать больше заявок, чем способен обработать consumer. Причем эта проблема становится актуальной лишь тогда, когда между producer и consumer возникает некоторая очередь, способная вобрать в себя большое количетсво заявок.

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

Итак, раз есть проблема, значит есть и очередь.

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

Этот же механизм может применяться и в SObjectizer-е с использованием mchain-ов (которые суть CSP-шные каналы, в чем-то аналогичные Go-шным). Т.е. если threadX общается со threadY через mchain фиксированного размера, то при попытке поставить новое сообщение в mchain поток threadX будет приостановлен. Хотя в SObjectizer-е возможны и другие реакции на попытку отослать сообщение в полный mchain.

Но если у нас не threadX и threadY, а actorX и actorY, и общаются они через свои штатные message-box-ы, то ситуация становится сложнее, т.к., в отличии от mchain-а, штатный message-box агента не имеет ограничений на количество сообщений. Поэтому actorX может наотсылать actorY больше сообщений, чем actorY в принципе может обработать.

Это хорошо известная для модели акторов проблема overload control. Решать которую можно по-разному, и в SObjectizer есть несколько готовых инструментов «искаропки».

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

eao197 ★★★★★
() автор топика
Ответ на: комментарий от rGlory

Если вам это не понятно, то хочется спросить - вы сами сложные программы на своей библиотеке строили? У вас акторы блокировались в обработке сообщений? Очень интересно послушать.

Представляете, строил. А если бы не строил, то не было SObjectizer-а в его нынешнем виде, с его нынешними возможностями.

Посему несколько слов о то, какие бонусы мы имеем, если используем для общения между сущностями именно асинхронные сообщения. Под сущностями будут пониматься как рабочие потоки (или fibers/coroutines/goroutines), так и акторы в виде объектов с callback-ами.

Под асинхронным понимается то, что когда X делает запрос Y, то X не блокируется на время, пока запрос дойдет до Y и будет обработан Y. Грубо говоря, у X и Y есть асинхронный send и (для простоты) синхронный receive.

Главный бонус в том, что когда X нужно что-то от Y, то X делает асинхронный запрос и может продолжать свою работу до тех пор, пока от Y не придет ответ или пока X не надоест ждать. Получается, что X не блокирован (на тему «блокирован» смотрим здесь), а значит может выполнять какую-то полезную работу.

Т.е. мы можем сделать что-то вроде:

void threadX(...) {
   // Сперва отправляем запрос Y.
   send(Y, ...);
   ... // Далее можем делать что-то свое.
   // Теперь нам нужен результат от Y, ждем его.
   auto reply = receive(...);
   ...
}

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

Так же мы можем в ожидании ответа от Y обрабатывать другие команды, которые приходят к X:

void threadX(...);
   // Отдаем запрос Y.
   send(Y, ...);
   // Ждем ответа и обрабатываем другие типы запросов.
   do {
      auto msg = receive(...);
      switch(msg) {
         ...;
      }
   } while(...);
}

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

При этом асинхронный send и синхронный receive дают нам, при желании, простую возможность программировать в синхронном стиле. Легким движением руки:

auto askY(...) {
   send(Y, ...);
   auto result = receive(...);
   if(!result)
      throw std::runtime_exception(...);
   return *result;
}
...
void threadX(...) {
   try {
      handle(askY(...));
   }
   catch(...) {
      ...
   }
}

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

eao197 ★★★★★
() автор топика
Ответ на: комментарий от rGlory

Ну и чтобы два раза не вставать, еще пару слов про «асинхронную лапшу».

Нужно разделять случаи, когда у нас есть последовательный поток исполнения (т.е. нить ОС или fiber/coroutine) и когда у нас объект с коллбэками.

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

А во втором случае нам нужно возвращать управление из коллбэка. Поэтому если на объектах с коллбэками актору X нужно что-то от актора Y, то в одном своем коллбэке актор X делает send(Y, ...). А реакцию на ответное сообщение нужно делать в другом коллбэке. А если нам еще и нужна реакция на отсутствие ответа, то появляется третий коллбэк. А если при этом X может еще и менять свое состояние в ожидании ответа, то и еще коллбэки.

Вот тут-то и появляется та самая асинхронная лапша. Для борьбы с которой могут потребоваться специальные инструменты.

Но главная причина этого вовсе не в асинхронных сообщениях, а именно в акторах в виде объектов с коллбэками. Если акторы представляются нитями или короутинами, то там этой самой «асинхронной лапши» поменьше.

Фокус, однако, в том, что для каких-то предметных областей акторы в виде объектов с коллбэками удобнее. Поскольку, как правило, в этих областях акторы являются конечными автоматами (а временами и сложными иерархическими КА) и должны обрабатывать разные типы входящих сигналов в каждом из своих состояний.

Поскольку корни у SObjectizer-а растут из одной из таких областей, то акторы в SObjectizer-е вот такие вот.

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

Короутины SObjectizer не поддерживает.

eao197 ★★★★★
() автор топика
Ответ на: комментарий от eao197

Все не читал, но вставлю свои пять копеек. Асинхронный код с колбеками можно писать и «последовательно» или, говоря более строго, писать «линейно». Для этого существуют всякие синтаксические сахары, такие частные по конкретную задачу как async-await, или общие для практически любых задач как do-expression для монад в Haskell и computation expressions в F# для монад и моноидов. В любой монаде по сути эти колбеки уже есть. Они как раз передаются вторым аргументов монадической связке. Если раскрывает синтаксический сахар сам компилятор, такой как хаскелевский GHC или компилятор F#, то вообще сложно на глаз различить асинхронный код от последовательного. Вот компилятор Rust еще хотят научить раскрывать async-await, но это уже частное решение конкретной задачи в отличие от тех же хаскеля и F#, которые предоставляют общее решение.

А так, модель актора/агента/эрланговского процесса - это очень классная вещь. Столько головной боли уходит при написании серверного кода. Только тут главное меру знать. А так, если есть изменяемое состояние, то агент очень удобен. Если нет изменяемого состояния, то бывает, что предпочтительнее использовать асинхронные вычисления, которые с этими самыми колбеками, но тут уже нужны грамотно настроенные пулы потоков, которые еще иногда называют «контекстами исполнения».

Единственное, что в C++ мне чего-то не нравится, как std::future описан в книгах и спецификациях. Вообще, у меня больше десятка лет такое чувство, что C++ уже давно в роли отстающего. И как-то некрасиво там делаются многие вещи. И колбеки там для future::then какие-то кривые. Ну, ладно! Оставим эту тему.

Желаю выдержки, силы воли и неугасающего интереса, чтобы ты мог развивать ваш проект дальше! Это хорошо, когда есть такое хобби, главное, чтобы оно не мешало другим интересам в жизни.

dave ★★★★★
()
Ответ на: комментарий от dave

Для этого существуют всякие синтаксические сахары, такие частные по конкретную задачу как async-await

Когда появляется синтаксический сахар, то разработчик перестает видеть коллбэки.

Единственное, что в C++ мне чего-то не нравится, как std::future описан в книгах и спецификациях.

SObjectizer — это как раз про другой подход к организации работы, нежели async-и и future. Это скорее про то, что я описывал в этой статье: https://habr.com/ru/post/335304/

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

Изначально операции должны были быть представлены агентами. Но со временем мы добавили и поддержку CSP, так что можно и вообще без агентов.

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

Желаю выдержки, силы воли и неугасающего интереса, чтобы ты мог развивать ваш проект дальше!

Спасибо!

Это хорошо, когда есть такое хобби

Это не хобби.

eao197 ★★★★★
() автор топика
Ответ на: комментарий от eao197

Akka

ох не все так просто со акка акторами.

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

Из проблем, например, навскидку, за акторами надо следить (делается отдельным бойлерплейтом с супервизором), там нельзя задать политику backpressure (решается через акка стримы, например, но - сюрприз - акторы не рекомендуется использовать, потому что нет гарантии доставки, если она важна), постоянные прохеренные сообщения из за неучтенных кейсов в receive, скорость работы тоже в общем не то чтобы прям реактивная (если важно).

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

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

arcanis ★★★★
()
Последнее исправление: arcanis (всего исправлений: 1)
Ответ на: комментарий от arcanis

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

Я бы добавил сюда еще и то, что в Модели Акторов взаимодействие ведется в режиме 1:1, а если нужно M:N, то нужно делать промежуточных акторов-брокеров и запариваться на подписку/отписку (что с учетом асинхронности и возможных потерь сообщений может изрядно доставить).

Поэтому у нас не только (да и не столько) Модель Акторов во главе угла. Скорее замес из Actors, Publish/Subscribe и CSP. Вот это все вместе и упрощает жизнь разработчику если в его программе оказываются нужны автономные самостоятельные сущности и асинхронное взаимодействие между ними.

Для борьбы с overload у нас есть message-limits, а для back-pressure — mchains. Вообще говоря, back-pressure на асинхронных сообщениях хорошо решается разве что в CSP. Но там же из-за back-pressure можно и на дедлоки нарваться.

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

Потери сообщений из-за неправильных подписок бывают, да. Одна из самых распространенных ошибок. Чтобы помочь с этим мы msg_tracing добавили. Можно отследить, что куда ушло и почему потерялась. Даже при отладке новых фич в SObjectizer-е этим регулярно пользуюсь.

Ненадежность доставки — это не баг, это фича. Асинхронщина вообще с гарантиями доставки не очень дружит. А, по опыту, ненадежность доставки парадоксальным (на самом деле нет) образом повышает надежность приложения.

На реактивность пока не жаловались. Хотя у нас достаточно высокие накладные расходы на доставку и вызов обработчика (это обратная сторона наличия mbox-ов, разнотипных диспетчеров и акторов в виде ИКА). Так что если кому-то нужны десятки миллионов сообщений в секунду на одного актора, то универсальный фреймворк вроде SObjectizer-а не подойдет.

eao197 ★★★★★
() автор топика
Ответ на: комментарий от eao197

Я бы добавил сюда еще и то, что в Модели Акторов взаимодействие ведется в режиме 1:1, а если нужно M:N, то нужно делать промежуточных акторов-брокеров и запариваться на подписку/отписку (что с учетом асинхронности и возможных потерь сообщений может изрядно доставить).

ну конкретно в случае M:N что то типа общей шины (event bus в реализации акка) позволяет решить задачу в ряде случаев (у вас соответственно через producer/comsumer)

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

да, я видел выше и, из своей практики, согласен с таким подходом лол

Спасибо за развернутый ответ, видимо, надо ближе смотреть на +- реальных задачах

arcanis ★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.