воскресенье, 26 февраля 2017 г.

[life.work] Мои личные и субъективные впечатления от C++ Russia 2017

Сегодня утром вернулся из Москвы, с конференции C++ Russia 2017, на которой даже умудрился выступить с докладом (отдельное спасибо организаторам конференции за предоставленную возможность, надеюсь, обедню никому не испортил). С физической точки зрения поездка оказалась сложной и выматывающей, так что растекаться мыслею по древу просто нет сил. Посему несколько личных и субъективных впечатлений. Рассказов о докладах и других технических подробностей не будет.

20170226-123808-DSCF5970

среда, 22 февраля 2017 г.

[work.throughts] Что делать со сравнениями SObjectizer и C++ Actor Framework?

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

Суть вот в чем: при обсуждениях SO-5 на публике рано или поздно, в той или иной форме, но всплывает вопрос о том, а чем же SO-5 отличается от CAF? (Ну, или в несколько другой формулировке: чем SO-5 лучше CAF?). Но если три года назад ответ на этот вопрос был интересен мне самому и было какое-то желание участвовать в разговорах на эту тему, то теперь и вопрос нисколько не интересен, и желания отвечать на него нет.

Как по мне, SObjectizer и CAF -- это совершенно разные продукты. У них разная история, разные цели, разные способы достижения этих целей и разные подходы к развитию, да и к самой разработке. Объединяет их одно: оба фреймворка предоставляют реализации Модели Акторов для C++. Но как раз таки эта единственная общая черта и приводит к интересу публики: мол, если и то, и другое реализует Модель Акторов, то в чем именно различия?

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

Только вот не смотря на осознание все этой резонности, желания ввязываться в сравнение двух этих фреймворков все равно нет. И здесь нельзя не отметить два, возможно, субъективных фактора.

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

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

Поэтому я склоняюсь к тому, чтобы перестать делать какие-то сравнения между SO-5 и CAF-ом. И, соответственно, перестать отвечать на вопросы о том, чем SO-5 лучше CAF и наоборот. Вместо этого, имхо, гораздо продуктивнее будет переводить разговоры в такую плоскость: "Расскажите, какого рода проблема перед вами стоит и я попробую рассказать, как вы сможете решить ее с помощью SObjectizer. Ну или объясню, почему в вашем случае SObjectizer не будет хорошим выбором".

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

В общем, есть большое желание не принимать участия в каких-либо сравнениях CAF-а с SObjectizer-ом (равно как и с другими реализациями). Но, блин, нет уверенности, что это правильно.

PS. Мы пробовали заслать на awesome-cpp PR с информацией о SObjectizer. Но его не приняли. Может быть, одного PR от аффилированных лиц было недостаточно. Поэтому буду признателен, если кто-нибудь из читателей найдет время дабы сделать PR, например, вот с таким текстом:

[SObjectizer](https://sourceforge.net/projects/sobjectizer) - A small framework with implementations of Actor, Pub/Sub and CSP models. [BSD]

воскресенье, 19 февраля 2017 г.

[prog.thoughts] Несколько соображений на тему "А о чем же Модель Акторов?"

В конце своего доклада на C++ CoreHard Winter 2017 я сказал о том, что если кто-то ждет высокой производительности от универсальных акторных фреймворков, то это напрасно. И добавил, что SObjectizer -- это не про производительность, а про другое. Подразумевая при этом не только SObjectizer, но и вообще реализации Модели Акторов, включая Erlang и Akka. C CAF-ом ситуация, имхо, чутка другая -- они сами говорят про high performance, ну и флаг им в руки, ибо замеры говорят сами за себя ;)

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

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

[life] Кратко личные впечатления о "C++ CoreHard Winter 2017"

Общее впечатление от прошедшей вчера конференции C++ CoreHard Winter 2017 Conference: было круто!

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

В общем, хочется выразить огромную благодарность и всему сообществу corehard.by за организацию этой конференции, и персонально Антону Наумовичу: ребята, вы большие молодцы, большое человеческое вам спасибо за проделанную работу!

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

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

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

пятница, 3 февраля 2017 г.

[life.business] Таки своя компания...

Много воды утекло с момента, когда в этом блоге появились первые размышления о создании собственной компании. Посеянные тогда зерна сомнений и надежд таки дали свои всходы и привели к тому, что своя компания у меня таки появилась. Осенью 2016-го года мы с несколькими бывшими коллегами создали ООО "СтифСтрим". Ну, или как задумывалось изначально, stiffstream.

Занимаемся, в первую очередь, SObjectizer-ом. По сути, смысл создания компании был в том, чтобы за SObjectizer-ом появилась некая коммерческая структура, которая могла бы заниматься предоставлением услуг вокруг SObjectizer-а на платной основе. Есть задумки по поводу пары-тройки продуктов над SObjectizer-ом, которые, вероятно, будут распространяться уже под двойной лицензией. Так же думаем эксплуатировать свой большой опыт в разработке софта вообще и софта на C++ в частности: обучение, консультации, небольшие разовые заказные разработки. В общем, если у вас что-то не идет с вашим C++ным кодом, то вы теперь знаете, к кому можно обратиться ;) Контрибьюторов в Boost среди нас нет, но работающий и понятный код на C++ мы писать умеем.

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

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

PS. По поводу названия. У слова stiff много смыслов в английском. Нас почти все устраивают :) Тем не менее, официальная трактовка -- это "тугой/жестки/несгибаемый/упертый поток". Те, кто меня знает и кто со мной плотно работал, думаю, не удивятся "жесткости" и "упертости" ;)

среда, 1 февраля 2017 г.

[prog.c++] Небольшой сниппет ко вчерашней теме (порционное чтение из CSP-канала с учетом тайм-аута)

При написании вчерашнего поста я что-то затормозил. На самом деле организовать чтение пакетов из не более чем 100 сообщений из CSP-канала так, чтобы с момента получения первого сообщения прошло не более 100ms, совсем не сложно. Для этого не требуется второй вспомогательный канал. Достаточно одного канала. Но читать из него нужно дважды:

  • сначала читается первое сообщение из будущего пакета без ограничения времени;
  • затем читаются остальные сообщения будущего пакета но так, чтобы общее время на их чтение не превышало 100ms.

У нас в SO-5 это выглядит вот так:

   for(;;) {
      // Ожидаем первого сообщения.
      const auto r = receive(chain, infinite_wait,
            [&](const kafka_msg & msg) { buffer.push_back(msg); });

      // Если сообщение действительно пришло, то ждем либо остальные
      // сообщения, либо завершение тайм-аута.
      if(r.handled()) {
         receive(from(chain).handle_n(buffer_size - 1).total_time(100ms),
               [&](const kafka_msg & msg) { buffer.push_back(msg); });
         // На этот момент buffer точно не пуст, поэтому сохраняем его.
         persist(buffer);
      }
      else
         // Ничего не вычитали из канала т.к. он закрыт. Продолжать нет смысла.
         break;
   }

Тут первый receive выполняет ожидание и обработку всего одного сообщения. А вот второй receive либо обрабатывает 99 сообщений (т.е. формирует полный пакет), либо же работает 100ms (причем не суть важно на что тратится это время -- на обработку сообщений или же на их ожидание).

Я не очень хорошо знаю Go, но, по-моему, там в select-е можно задать тайм-аут только на ожидание сообщений. Так что SObjectizer-овский total_time() для Go-шного select-а нужно будет написать, из коробки в Go я такого не припомню. Если ошибаюсь, то поправьте меня, плиз.

Полный же код демонстрационного примера можно найти вот в этом репозитории.

Этот пример, кстати, демонстрирует возможность использования SObjectizer-5 для многопоточного программирования вообще без акторов. Только CSP-шные каналы.

При этом можно увидеть, что рукопашная работа с рабочими нитями требует изрядной осторожности. Все эти auto_join и auto_close -- они же не от хорошей жизни появились. А для того, чтобы при возникновении исключений каналы закрывались, а рабочие потоки join-ились. Из-за этого, кстати говоря, приходится сначала объявлять объекты типа std::thread и создавать auto_join-ер для них, потом создавать каналы и auto_closer-ы для каналов. И только после этого стартовать рабочие нити. Если поменять последовательность, то при возникновении исключения можно запросто повиснуть на join-е: рабочая нить не завершиться, поскольку никто не закроет канал, на котором она висит в receive.

Подробнее про mchain-ы в SObjectizer (которые и есть CSP-каналы) можно узнать в этой презентации или в этом разделе Wiki.

PS. Ну и не могу не сказать еще раз про современный C++. В примере используется C++14, работать с ним приятно. Теперь бы еще поднять скорость компиляции C++ного кода на порядок, вообще было бы здорово.

вторник, 31 января 2017 г.

[prog.flame] Непонятое из рассказа про миграцию со Scala на Go (глазами пользователя SObjectizer)

Нашел, кажется, на RSDN-е ссылку на прекрасное: "Making the move from Scala to Go, and why we’re not going back". Там люди рассказывают про то, как они писали-писали на Scala, задолбались и с удовольствием перешли на Go. От чего сейчас буквально писают кипятком очень счастливы.

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

В еще большем недоумении меня оставило другое: в статье описывается пример с чтением потока сообщения из Kafka и сохранения накопленных сообщений в БД. Люди попытались примерить к этой задачке Модель Акторов в реализации Akka и что-то у них не получилось. Дословно одна из причин недовольства Akka описана так: "Furthermore, the stream came from a Kafka Consumer, and in our wrapper we needed to provide a `digest` function for each consumed message that ran in a `Future`. Circumventing the issue of mixing Futures and Actors required extra head scratching time." Честно говоря я не понимаю, о чем это все.

Но если посмотреть на пример того, как они обозначают решение этой же задачи на потоках в Go, то мы видим вот что:

buffer := []kafkaMsg{}
bufferSize := 100
timeout := 100 * time.Millisecond

for {
  select {
    case kafkaMsg := <-channel:
      buffer = append(buffer, kafkaMsg)
      if len(buffer) >= bufferSize {
        persist()
      }
    case<-time.After(timeout):
      persist()
  }
}

func persist() {
      insert(buffer)
      buffer = buffer[:0]
}

Честно говоря, я не понимаю, откуда могут возникнуть затруднения в реализации этого же на акторах. Вот, скажем, у нас в SObjectizer-5 это могло бы выглядеть вот так:

class kafka_msg_persister final : public so_5::agent_t {
   std::vector< kafka_msg > buffer_;
   static constexpr std::size_t buffer_size = 100;
   so_5::timer_id_t dump_pause_;

public :
   kafka_msg_persister(context_t ctx) : so_5::agent_t(std::move(ctx)) {
      struct dump_by_timer : public so_5::signal_t {};

      so_subscribe_self()
         .event([this](const kafka_msg & msg) {
            buffer_.push_back(msg);
            if(buffer_.size() >= buffer_size)
               persist();
            else
               dump_pause_ = so_5::send_periodic<dump_by_timer>(*this, 100ms, 0ms );
         })
         .event<dump_by_timer>([this]{ persist(); });
   }

private :
   void persist() {
      insert(buffer_);
      buffer_.clear();
      dump_pause_.reset();
   }
};

Если я правильно понимаю пример кода на Go выше, то логика у него какая-то странная: сохранение сообщений в БД осуществляется либо когда буфер полностью заполняется, либо же спустя 100ms после получения последнего сообщения. Странность здесь такая. Обычно тайм-аут для сохранения попавших в буфер сообщений отсчитывают от момента получения первого сообщения. А не от момента получения последнего. Ведь если у нас, скажем, есть поток сообщений с темпом 90ms, то первое сообщение будет сохранено в БД только спустя 9000ms (т.е. 9s) после получения.

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

class kafka_msg_persister2 final : public so_5::agent_t {
   std::vector< kafka_msg > buffer_;
   static constexpr std::size_t buffer_size = 100;
   so_5::timer_id_t dump_pause_;

public :
   kafka_msg_persister2(context_t ctx) : so_5::agent_t(std::move(ctx)) {
      struct dump_by_timer : public so_5::signal_t {};

      so_subscribe_self()
         .event([this](const kafka_msg & msg) {
            if(buffer_.empty())
               dump_pause_ = so_5::send_periodic<dump_by_timer>(*this, 100ms, 0ms );

            buffer_.push_back(msg);
            if(buffer_.size() >= buffer_size)
               persist();
         })
         .event<dump_by_timer>([this]{ persist(); });
   }

private :
   void persist() {
      insert(buffer_);
      buffer_.clear();
      dump_pause_.reset();
   }
};

Либо же можно было бы использовать факт того, что агент в SO-5 -- это конечный автомат, на переходы между состояниями которого можно повесить обработчики:

class kafka_msg_persister3 final : public so_5::agent_t {
   std::vector< kafka_msg > buffer_;
   static constexpr std::size_t buffer_size = 100;

   state_t empty{this}, not_empty{this};

   so_5::timer_id_t dump_pause_;

public :
   kafka_msg_persister3(context_t ctx) : so_5::agent_t(std::move(ctx)) {
      struct dump_by_timer : public so_5::signal_t {};

      this >>= empty;

      empty
         .on_enter([this]{ dump_pause_.reset(); })
         .transfer_to_state<kafka_msg>(not_empty);

      not_empty
         .on_enter([this]{
            dump_pause_ = so_5::send_periodic<dump_by_timer>(*this, 100ms, 0ms );
         })
         .event([this](const kafka_msg & msg) {
            buffer_.push_back(msg);
            if(buffer_.size() >= buffer_size) {
               persist();
               this >>= empty;
            }
         });
         .event<dump_by_timer>([this]{ persist(); });
   }

private :
   void persist() {
      insert(buffer_);
      buffer_.clear();
   }
};

Т.е. при входе в состояние not_empty мы взводим таймер на 100ms, при входе в empty сбрасываем таймер, поскольку сейчас он нам уже не нужен.

Очевидно, что примеры на C++ и SO-5 более многословны, чем код на Go. Но задача была в том, чтобы показать, что на акторах накопление сообщений в буфер и затем сброс накопленных сообщений в БД по таймеру или по исчерпанию буфера -- это не сложно. Откуда у людей с этим возникли проблемы не понятно. Может быть дело в Akka, может быть они просто Akka готовить не умеют. Не знаю.

Очевидно, что есть люди, которым модель акторов в принципе не нравится. И которые предпочитают использовать CSP-ные каналы. Ну чтож, попробуем изобразить этот же пример на CSP-шных каналах, в SObjectizer-5:

std::vector<kafka_msg> buffer;
static constexpr std::size_t buffer_size = 100;

for(;;) {
   receive(from(chain).handle_n(buffer_size).empty_timeout(100ms),
      [&](const kafka_msg & msg) {
         buffer.push_back(msg);
      });
   if(!buffer.empty())
      persist(buffer);
}

Здесь мы выходим из receive либо после получения 100 сообщений kafka_msg, либо после того, как канал был пуст в течении 100ms. Как раз то, что было в исходном примере на Go.

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

std::vector<kafka_msg> buffer;
static constexpr std::size_t buffer_size = 100;

struct dump_by_timer : public so_5::signal_t {};
auto timer_chain = create_mchain(env);

select( so_5::from_all(),
   case_(chain, [&](const kafka_msg & msg) {
         if(buffer.empty())
            so_5::send_delayed<dump_by_timer>(timer_chain, 100ms);
         buffer.push_back(msg);
         if(buffer.size() >= buffer_size)
            persist(buffer);
      }),
   case_(timer_chain, [&](mhood_t<dump_by_timer>) {
         if(!buffer.empty())
            persist(buffer);
      }) );

Но здесь возможно множественное срабатывание таймеров. Например, если идет очень большая последовательность сообщений, то на каждое 1-ое, 101-ое, 201-ое и т.д. сообщения будет отсылаться отложенный сигнал. А потом в какой-то прекрасный момент эти сигналы начнут приходить. Что не есть большая проблема, но и не есть хорошо. Поэтому придется немного поколупаться с отменой ранее отосланных отложенных сигналов. И более-менее реальный код выглядел бы как-то так:

std::vector<kafka_msg> buffer;
static constexpr std::size_t buffer_size = 100;

struct dump_by_timer : public so_5::signal_t {};

auto timer_chain = create_mchain(env,
      // Нам нужно хранить не более одного сигнала в этом канале.
      1,
      // Место под сигнал выделим сразу.
      so_5::mchain_props::memory_usage_t::preallocated,
      // При поступлении нового сигнала старый выбрасываем за ненадобностью.
      so_5::mchain_props::overflow_reaction_t::remove_oldest);

// Идентификатор таймера нам нужен дабы была возможность отменять
// доставку отложенного сигнала.
so_5::timer_id_t dump_timer;

select( so_5::from_all(),
   case_(chain, [&](const kafka_msg & msg) {
         if(buffer.empty())
            dump_timer = so_5::send_periodic<dump_by_timer>(timer_chain, 100ms, 0s);
         buffer.push_back(msg);
         if(buffer.size() >= buffer_size) {
            dump_timer.reset();
            persist(buffer);
         }
      }),
   case_(timer_chain, [&](mhood_t<dump_by_timer>) {
         if(!buffer.empty())
            persist(buffer);
      }) );

К чему я это все веду (ну, естественно, за вычетом маркетинга SO-5)? К тому, что язык высокого уровня дает разработчику возможность создавать те абстракции, которые ему нужны для решения задачи. Нужны акторы -- можно сделать акторов, нужны CSP-шные каналы -- можно сделать каналы. Если же возможность по созданию абстракций под задачу не нужна, значит задача вполне себе решается более простым и примитивным языком. Т.е. изначально людям нужен был Go, а не Scala. И нахрена было тянуть в проект Scala, дабы затем плакаться о том, что "кололись, но жрали" -- не понятно.

Впрочем, если посмотреть на бэкграунд разработчиков:

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