четверг, 13 апреля 2017 г.

[prog.c++] А кому-нибудь нужна поддержка MSVS2013 (aka VC++12) в SObjectizer?

Вопрос более чем серьезный. При работе над новой версией SO-5.5.19 приходится много работать с шаблонами и проблемы с уровнем поддержки С++11 в MSVS2013 стали уже совсем комом в горле.

Если MSVS2013 никому не нужен, то мы бы могли ограничиться только MSVS2015 и MSVS2017.

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

вторник, 11 апреля 2017 г.

[prog.flame] C++ никто не заменит! C++ незаменим! :)

Ибо в каком еще языке можно выражать свои мысли столь витиевато и велеречиво?

templatetypename T >
struct message_payload_type
   :  public message_payload_type_impl< T,
         is_classical_message<
               typename ::so_5::details::message_mutability_traits<T>::payload_type >::value >
   {
   };

Ну и да, шаблоны в C++ -- это чистой воды программирование на динамически-типизированном языке: пока unit-тестами все не покроешь, не можешь быть уверен в правильности работы :)

понедельник, 10 апреля 2017 г.

[prog.thoughts] Очередная итерация вокруг movable/modifiable сообщений. Нужно сделать непростой выбор из двух(?) вариантов...

Тема movable/modifiable сообщений для SObjectizer-а, поднятая некоторое время назад, и, казалось бы, начавшая уже самостоятельно дышать, по мере своего развития стала получать серьезные удары. Один из них был обозначен вчера. Сегодня случился еще один. Посему есть желание еще раз подумать о том, а не следует ли что-нибудь подправить в консерватории?

Итак, был попробован подход, в котором создавались пары похожих, но несовместимых между собой сущностей: mbox_t и unique_mbox_t, mchain_t и unique_mchain_t.

Основное достоинство этого подхода в том, что в compile-time контролируется невозможность отсылки shared-сообщения в unique_mbox/mchain. Это очень сильное преимущество в сравнении с другими вариантами, поэтому именно с этого подхода и началась попытка реализации movable/modifiable сообщений в SO-5.5.19. Однако, чем дальше, тем больше шансов, что это достоинство будет нивелировано необходимостью иметь дело с разными типами mbox-ов и mchain-ов. Т.е. там, где раньше разработчик просто использовал mbox_t, сейчас ему придется заботиться и о mbox_t, и о unique_mbox_t. Например, если раньше могло быть что-то вроде:

template<typename MSG, typename... ARGS>
void make_and_send_to(
   const mbox_t & dest,
   const some_common_arg_type & common_arg,
   ARGS && ...args)
{
   so_5::send<MSG>(to, common_arg, std::forward<ARGS>(args)...);
}
...
if(has_free_workers)
   make_and_send_to<handle_request>(processor, request);
else if(can_be_delayed(request))
   make_and_send_to<delay_request>(wait_queue, request, delaying_options);
else
   make_and_send_to<reject_request>(rejector, request, no_free_workers, unable_to_delay, default_error_code);

То сейчас dest придется передавать шаблонным параметром. Т.е. делать что-то вроде:

template<typename MSG, typename MBOX_HANDLE, typename... ARGS>
void make_and_send_to(
   const MBOX_HANDLE & dest,
   const some_common_arg_type & common_arg,
   ARGS && ...args)
{
   so_5::send<MSG>(to, common_arg, std::forward<ARGS>(args)...);
}
...

К сожалению, в таком виде в качестве первого параметра для make_and_send_to можно будет передать вообще все, что угодно. Даже int. И получить затем портянку ошибок с отсутствием должного варианта so_5::send().

Но если в случае с mbox_t/unique_mbox_t деление на shared- и unique-mbox еще оправдано наличием как взаимодействия 1:N, так и 1:1, то вот для mchain-ов разделение на обычный (т.е. shared-) и unique-mchain уже достаточно сложно объяснить. Поскольку главная причина такого деления возникает из-за необходимости сохранять совместимость с предшествующими версиями SO-5, а отнюдь не из-за требований какой-то общепризнанной теоретической модели.

Поэтому я все больше и больше склоняюсь к тому, чтобы текущее направление развития movable/modifiable сообщений признать тупиковым. И сделать вариант, который основан на маркере unique_msg при отсылке и при получении сообщения.

Так, для того, чтобы отослать unique-сообщение единственному получателю потребуется сделать вызов so_5::send<so_5::unique_msg<M>>(dest, args...). А для получения сообщения нужно будет использовать so_5::mhood_t<so_5::unique_msg<M>>. Для обычных агентов это может выглядеть, например, так:

class demo final : public so_5::agent_t {
public :
   demo(context_t ctx) : so_5::agent_t(std::move(ctx)) {
      so_subscribe_self()
         // Подписываем агента на иммутабельное сообщение типа K.
         .event([this](const K & cmd) {...})
         // Подписываем агента на иммутабельное сообщение типа L.
         .event([this](mhood_t<L> cmd) {...})
         // Подписываем агента на мутабельное сообщение типа M.
         .event([this](mhood_t<unique_msg<M>> cmd) {...});
   }

   virtual void so_evt_start() override {
      // При старте отсылаем самому себе сообщения K, L и M.
      // Сообщения K и L идут как иммутабельные.
      so_5::send<K>(*this, ...);
      so_5::send<L>(*this, ...);
      // Сообщение M идет как мутабельное.
      so_5::send<unique_msg<M>>(*this, ...);
   }
};

При этом можно обратить внимание на то, что и иммутабельные, и мутабельное сообщение отсылается в один и тот же direct-mbox агента-получателя. В первом подходе, который уже частично реализован, это невозможно. Там нужно для агента создавать отдельный unique_mbox, подписывать обработчик события на этот unique_mbox и сообщение отсылать не в обычный direct_mbox, а в отдельный unique_mbox.

Для mchain-ов может выглядеть так:

auto ch = create_mchain(env);

// Отсылаем три сообщения.
// Сообщения K и L идут как иммутабельные.
so_5::send<K>(ch, ...);
so_5::send<L>(ch, ...);
// Сообщение M идет как мутабельное.
so_5::send<so_5::unique_msg<M>>(ch, ...);

// Обрабатываем все сообщения из канала.
receive(from(ch),
   // Обработчик для иммутабельного K.
   [](const K & cmd) {...},
   // Обработчик для иммутабельного L.
   [](so_5::mhood_t<L> cmd) {...},
   // Обработчик для мутабельного M.
   [](so_5::mhood_t<so_5::unique_msg<M>> cmd) {...});

В общем, как мне думается, все достаточно наглядно и видно, где отсылается unique-сообщение и где ожидается именно unique-сообщение.

Однако, вот что в таком подходе не нравится:

  • нет проверок в compile-time. Т.е. если пользователь сделает send<unique_msg<M>> в multi-producer/multi-consumer mbox, то получит исключение в run-time, а не ошибку во время компиляции.
  • в один и тот же mbox можно отослать и иммутабельное, и мутабельное сообщение типа M, и эти экземпляры попадут в разные обработчики:

    so_5::send<M>(ch, ...);
    so_5::send<so_5::unique_msg<M>>(ch, ...);
    receive(from(ch),
       [](so_5::mhood_t<M>) { std::cout << "immutable M" << std::endl; },
       [](so_5::mhood_t<so_5::unique_msg<M>>) { std::cout << "mutable M" << std::endl; });

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

    // Отсылаем как иммутабельное сообщение.
    so_5::send<M>(ch, ...);
    ...
    receive(from(ch),
       // А получить пытаемся как мутабельное.
       [](so_5::mhood_t<so_5::unique_msg<M>>) {...});

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

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

Ранее я так же думал, что у этого подхода есть еще недостаток, связанный с тем, что различия между иммутабельными и мутабельными сообщениями могут мешать писать обобщенный код, вроде показанной выше шаблонной функции make_and_send_to. Однако, затем подумалось, что если ввести шаблон immutable_msg<M> то с его помощью можно сделать вызовы send<M> и send<immutable_msg<M>> синонимами. Что позволит писать обобщенный код вот в таком виде:

template<typename MSG, typename... ARGS>
void make_and_send_to(
   const mbox_t & dest,
   const some_common_arg_type & common_arg,
   ARGS && ...args)
{
   so_5::send<MSG>(to, common_arg, std::forward<ARGS>(args)...);
}
...
if(has_free_workers)
   make_and_send_to<unique_msg<handle_request>>(processor, request);
else if(can_be_delayed(request))
   make_and_send_to<unique_msg<delay_request>>(wait_queue, request, delaying_options);
else
   make_and_send_to<immutable_msg<reject_request>>(rejector, request, no_free_workers, unable_to_delay, default_error_code);

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

Итак, сейчас стоит дилемма: продолжать ли непростой путь в сторону mbox/unique_mbox+mchain/unique_mchain или же нужно начать все сначала и двинуться в сторону send<unique_msg<M>>? Если у кого-то есть соображения, то прошу высказываться. Реальная возможность повлиять на вектор развития SObjectizer-а :)

PS. По ходу написания текста поста сам себя поймал на том, что лучше оперировать понятиями immutable_msg и mutable_msg, нежели понятиями shared- и unique-сообщения. Так что если второй подход получит право на жизнь, то будет использоваться нотация send<mutable_msg<M>>.

UPD. Еще один большой минус наличия mbox/unique_mbox и mchain/unique_mchain, это усложнение жизни тех пользователей, которые делают собственные реализации mbox-ов и mchain-ов. Например, собственная реализация mbox-а может понадобиться для балансировки нагрузки: пользователь может написать свой mbox, который будет адресовать очередное сообщение тому агенту, который меньше загружен. Соответственно, при появлении деления на mbox/unique_mbox такие трюки будет проделывать сложнее.

воскресенье, 9 апреля 2017 г.

[prog] На тему сложности проектирования: внезапная проблема в попытке подружить unique-сообщения и message chains

Продолжение темы добавления в SO-5 такой штуки, как unique-сообщения. Unique-сообщение -- это сообщение, которое:

  • идет строго одному получателю (т.е. используется исключительно при взаимодействии 1:1);
  • дает получателю право менять экземпляр сообщения как ему вздумается (это нужно для ситуаций, когда сообщение большое и его нужно править "по месту", либо же когда в сообщении перемещаются move-only данные, которые из сообщения нужно забрать, например, в сообщении передается экземпляр типа File или unique_ptr).

Для того, чтобы unique-сообщения можно было отсылать агентам, в SO-5.5.19 добавляется новый тип почтового ящика -- unique_mbox. Только при подписке на unique_mbox можно повесить обработчик для unique-сообщения. Таким образом еще в компайл-тайм обеспечивается гарантия того, что нельзя взять shared-сообщение из обычного mbox-а, изменить это сообщение и отправить измененный экземпляр другому агенту.

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

Мне показалось разумным, что периодическое сообщение не может быть unique-сообщением. Поскольку:

  • получатель сообщения не является единоличным владельцем указателя на экземпляр сообщения. Еще один указатель есть у таймера. Соответственно, может получиться ситуация, когда получатель в момент времени T обрабатывает экземпляр периодического сообщения M (которое ему было отослано таймером некоторое время назад, но долго простояло в очереди), а в этот же момент таймер ставит в очередь получателя этот же самый экземпляр сообщения еще раз. Получится, что получаетль будет править объект, который находится у него в очереди. Что, может и не страшно, но как-то странно, по меньшей мере;
  • получатель сообщения не имеет препятствий к тому, чтобы переслать этот экземпляр unique-сообщения кому-то другому. Но, в этом случае сообщение в прямом смысле перестает быть unique-сообщением, т.к. оно в буквально начинает адресоваться сразу двум получателям. Первый -- это исходный получатель, которому периодическую доставку сообщения осуществляет таймер. Второй -- это тот, кому первый получатель переадресовал сообщение.

Поэтому на уровне API в SO-5.5.19 отсылка периодических сообщений в unique_mbox сейчас просто запрещена. И это правильно, как мне думается.

Теперь пришло время подружить unique-сообщения с message chains (mchains в терминологии SO-5). Mchains -- это реализация концепции каналов из модели CSP. При этом сообщения из mchain-ов всегда доставляются только одному получателю, этим mchain-ы принципиально отличаются от multi-producer/multi-consumer mbox-ов.

Казалось бы, раз mchain -- это всегда multi-producer/single-consumer канал, то нет смысла создавать дихотомию shared_mchain (или обычный mchain) и unique_mchain. Можно просто разрешить пользователю отсылать и получать unique-сообщения в обычный mchain. Т.е. можно просто выполнить send<Msg>(mchain,params) и получить этот Msg через unique_mhood_t<Msg> (т.е. получить сообщение как unique-сообщение).

Однако, здесь встречаются грабли, связанные с периодическими сообщениями. Сейчас можно отослать периодическое сообщение в mchain. Но, если любое сообщение из mchain можно получить через unique_mhood_t (т.е. как unique-сообщение), то значит и периодическое сообщение так же может быть извлечено как unique-сообщение. Что не есть хорошо (см. две вышеобозначенные причины).

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

Но тогда возникает вполне резонный вопрос: а как объяснять пользователю, зачем нужны mchain и unique_mchain, если по сути это одно и то же? И есть подозрение, что объяснить будет не так-то просто. А раз так, то пользователю будет сложнее использовать SO-5, а это не есть хорошо.


Возможно, подходить к проблеме unique-сообщений нужно было по-другому. Сейчас описанные выше сложности возникают из-за того, что исходная отсылка shared-сообщения и unique-сообщения ничем не отличается. Соответственно, когда в очередь агента или в буфер mchain-а стал объект типа Msg, то при вызове обработчика этого сообщения есть только объект Msg, но нет информации о том, отсылался ли этот объект как shared-сообщение или как unique-сообщение. Отсюда и возникающие сложности.

Можно было бы пойти другим путем. Например, заставить пользователя для отсылки unique-сообщений использовать специальную шаблонную обертку, вроде unique_msg<Msg>. Соответственно, не потребовалось бы делить mbox-ы на обычные mbox-ы и unique_mbox-ы. Не пришлось бы думать над тем, нужно ли делить mchain-ы на обычные mchain-ы и unique_mchain-ы. Запрет отсылки периодических unique-сообщений решался бы не по типу получателя (mbox/unique_mbox или mchain/unique_mchain), а по типу сообщения -- если отсылается unique_msg<Msg>, значит запрет.

Однако, такой подход так же имеет свои слабые стороны:

  • попытку отослать unique-сообщение в multi-producer/multi-consumer mbox можно было бы обнаружить только в run-time. А ошибка в run-time гораздо хуже, чем ошибка в compile-time;
  • усложнилось бы написание шаблонного кода. Допустим, сейчас можно написать шаблонную функцию make_and_send_msg, которая будет конструировать какое-то прикладное сообщение и отсылать его в указанный канал или mbox. При наличии шаблона-обертки unique_msg<Msg> мы уже не сможем просто так использовать эту функцию для отсылки как shared-, так и unique-сообщений.

Поэтому-то сейчас и был использован подход на базе дихотомии mbox/unique_mbox, mchain/unique_mchain.


Для чего был написан этот пост? Прежде всего для того, чтобы проверить, правильно ли я сам для себя понимаю возникшую проблему. Если бы я ее недостаточно понимал, то связного текста просто бы не получилось.

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