На Хабре обнаружилась статья, в которой рассматриваются реализации на 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; } ); if( 0 == 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 будет выглядеть еще что-нибудь, то дайте знать: попробуем сделать и показать.
Комментариев нет:
Отправить комментарий