среда, 30 марта 2016 г.

[prog.c++14] Хабровские примеры из батла Go vs D, но на SO-5.5.16

На Хабре обнаружилась статья, в которой рассматриваются реализации на Go и D одних и тех же простеньких примеров из области многопоточности.

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

Для чего на SO-5 было реализована два примера. Краткий их разбор под катом. Взять поиграться их можно из github-овского репозитория (для сборки нужны Ruby+rake+Mxx_ru, заодно там возможности MxxRu::externals можно увидеть).

Итак, первый пример -- это использование гороутин, каналов и select-а для вычисления значений из ряда Фибоначчи. На SO-5 этот пример выглядит вот так:

#include <so_5/all.hpp>

#include <chrono>

using namespace std;
using namespace std::chrono_literals;
using namespace so_5;

struct quit {};

void fibonacci( mchain_t values_ch, mchain_t quit_ch )
{
   int x = 0, y = 1;
   mchain_receive_result_t r;
   do
   {
      send< int >( values_ch, x );
      auto old_x = x;
      x = y; y = old_x + y;

      r = receive( quit_ch, no_wait, [](quit){} );
   }
   while( mchain_props::extraction_status_t::chain_closed != r.status() && 1 != r.handled() );
}

int main()
{
   wrapped_env_t sobj;

   thread fibonacci_thr;
   auto thr_joiner = auto_join( fibonacci_thr );

   auto values_ch = create_mchain( sobj, 1s, 1,
         mchain_props::memory_usage_t::preallocated,
         mchain_props::overflow_reaction_t::abort_app );

   auto quit_ch = create_mchain( sobj );
   auto ch_closer = auto_close_drop_content( values_ch, quit_ch );

   fibonacci_thr = thread{ fibonacci, values_ch, quit_ch };

   receive( from( values_ch ).handle_n( 10 ), []( int v ) { cout << v << endl; } );

   send< quit >( quit_ch );
}

Вычисление значений взято в точности из Go-шного примера, поэтому первым значением будет 0.

Т.е. сопрограмм (аналогов гороутин) в C++ и в SO-5 нет, то используется обычный поток, в котором вычисляется очередное значение и делается попытка записать его в канал со значениями из ряда Фибоначчи.

Как раз здесь и кроется главное отличие SO-5 варианта от Go-шного. В SO-5 есть select, но этот select работает только на чтение из mchain-ов. А в Go в select можно передать и операцию записи в канал. Поэтому в Go-шном варианте используется цикл с select-ом внутри, а в SO-шном варианте цикл с одним receive.

SO-5 вариант работает следующим образом: операция send завершается либо если удалось записать значение в канал, либо если канал закрыт. Если же канал полон, то send блокирует текущую нить. Когда send возвращает управление, проверяется наличие сообщения quit в quit-канале. Если оно там есть или если quit-канал оказался закрыт, то работа нити по генерации чисел Фибоначчи завершается. Таким образом, для завершения работы этой нити обязательно нужно закрыть values-канал и отослать сообщение quit в quit-канал (либо просто закрыть quit-канал).

Так что SO-шный вариант нельзя считать полным аналогом Go-шного, т.к. в SO-5 нельзя совместить send и select. Тем не менее, первые N значений из ряда Фибоначчи генерируются в одной нити и через канал вычитываются в другой нити. Команда на завершение генерации так же выдается посредством каналов.

Канал получения чисел из ряда Фибоначчи сделан с ограничением размера: там может уместиться всего одно значение. Поэтому нить генерации чисел будет засыпать на send-е, если главная нить не вычитывает очередное значение. Спать на send-е нить генерации будет не более 1s. Если за это время место в values-канале не появилось, то работа примера будет прервана abort-ом. Это один из способов защиты приложений от перегрузки ;)

Замечу еще, что реализацию функции main можно было бы сделать короче. Но тогда не были бы учтены возможные исключения. В принципе, для такого маленького примера на исключения можно было не заморачиваться. Тем не менее, привычка делать нормальные вещи взяла свое, поэтому и появились все эти auto_joiner-ы и auto_closer-ы.


Второй пример -- это аналог Go-шного примера для демонстрации default section. Заодно он показывает, как отложенные и периодические сообщения отсылаются в mchain-ы посредством send_delayed и send_periodic:

#include <so_5/all.hpp>

#include <chrono>

using namespace std;
using namespace std::chrono_literals;
using namespace so_5;

int main()
{
   struct tick {};
   struct boom {};

   wrapped_env_t sobj;

   auto ch = create_mchain( sobj );

   auto tick_timer = send_periodic< tick >( ch, 100ms, 100ms );
   send_delayed< boom >( ch, 500ms );

   bool boom_received = false;
   while( !boom_received )
   {
      auto r = receive( ch, no_wait,
            [](tick) { cout << "tick." << flush; },
            [&boom_received](boom) {
               cout << "BOOM!" << flush;
               boom_received = true;
            } );
      if0 == r.handled() )
      {
         cout << "    ." << flush;
         this_thread::sleep_for( 50ms );
      }
   }
}

Поскольку здесь используется всего один канал, то вместо select-а задействован обычный receive, в который передаются обработчики для разных типов сообщений. Вызов receive делается с параметром no_wait, поэтому receive сразу же возвращает управление, если в канале ничего нет. Что дает нам возможность выполнить те действия, которые в Go-шном варианте выполняются в default section. В остальном, вроде бы, ничего сложного, все довольно тривиально.

Пожалуй, единственный момент, который стоит подчеркнуть -- это сохранение значения, возвращенного вызовом send_periodic. Возвращается специальный объект типа timer_id_t, деструктор которого уничтожает периодическое сообщение. Поэтому, если мы не хотим сразу же отменить периодическую доставку сообщения tick, то нужно этот объект сохранить. Отсюда и переменная tick_timer.


Какие выводы напрашиваются?

Вероятно, функции receive и select в SO-5 нужно снабдить возможностью навесить обработчик на факт закрытия канала. Что-то вроде:

receive(
   from( quit_ch ).notify_on_close().no_wait_on_empty()
   [](quit){ ... },
   [](const mchain_closed &) { ... });

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

Нужно ли добавлять аналог default section в receive и select? Пока не очевидно. В принципе, сделать это не сложно. Что-то вроде:

receive(
   from( quit_ch ).default_section( []{ ... /*some code*/ } ),
   [](quit){ ... },
   [](const mchain_closed &) { ... });

А вот имеет ли смысл совмещать в SO-5 операцию select не только с чтением из канала, но и записью в канал... Вот это не очевидно. Т.к. send предназначен для асинхронного обмена сообщениями между агентами. А mchain-ы служат всего лишь нескольким вспомогательным целям. И есть сомнения в том, что в реальном боевом коде на SO-5 кто-нибудь будет делать что-то вроде:

select(
   from_all(),
   send_<int>( values_ch, x ),
   case_( quit_ch, [](quit){} ) );

Впрочем, обычный же select был сделан. Так что если кому-то select на send-ах действительно нужен, то можно будет попробовать реализовать его в версии 5.5.17. Но только если будут показаны какие-то более-менее правдоподобные сценарии. А то втаскивать в SObjectizer сложные фичи просто ради маркетинга нет возможности.

PS. Напоследок замечу, что в репозиторий so5-vs-others со временем будут добавляться и другие примеры (тот же concurrent hello_world первый кандидат на добавление). А если кто-то хочет посмотреть, как на SO-5 будет выглядеть еще что-нибудь, то дайте знать: попробуем сделать и показать.

Комментариев нет: