пятница, 25 июля 2014 г.

[prog.c++] Досадный баг в MSVC++ 2013

Современный C++ позволяет писать довольно лаконично. Например, если есть вот такая структура с функциями-фабриками:

struct factories_t
{
   std::function< dispatcher_unique_ptr_t() > m_disp_factory;
   std::function< binder_unique_ptr_t() > m_bind_factory;
};

То можно написать вот такую функцию, которая создает экземпляры данной структуры:

factories_t
create_factories( const std::string & name )
{
   if"first" == name )
      return {
         []() { return create_first_dispatcher(); },
         []() { return create_first_binder(); }
      };

   return {
      []() { return create_second_dispatcher(); },
      []() { return create_second_binder(); }
   };
}

К сожалению, в MSVC++ 2013 подобный код работает только, если написанные в create_factories() лямбды не требуют захвата каких-либо переменных. Если же требуют, т.е. если написать лямбду вот так:

return {
   []() { return create_first_dispatcher(); },
   [name]() { return create_first_binder(); }
};

То при попытке обратиться к ней возникнет исключение с текстом "bad function call" (т.е., похоже, в этом случае лямбда не преобразуется корректно в объект std::function).

Между тем под GCC (4.8.3 и 4.9.0) нормально работают и вариант без захвата контекста, и вариант с захватом контекста.

Пичалька...

Под катом полный самодостаточный код теста для самостоятельной проверки.

[prog.c++] Небольшая демонстрация поведения разных диспетчеров в SObjectizer

В качестве демонстрации диспетчеров thread_pool и adv_thread_pool сделал сегодня небольшой пример, который имитирует некую длительную, блокирующую рабочую нить работу. В примере создается три агента:

Агент-менеджер, который отсылает N сообщений do_hardwork агенту-исполнителю, получает от исполнителя отчеты о проделанной работе (hardwork_done). Получая каждый экзепляр hardwork_done агент-менеджер отсылает сообщение check_hardwork третьему агенту -- агенту-контроллеру. В ответ контроллер должен прислать hardwork_checked. Когда на все N сообщений do_hardwork будут получены N сообщений hardwork_done и hardwork_checked, агент-менеджер отображает отчет о затраченном времени и завершает работу.

Агенты исполнитель и контроллер в своих обработчиках сообщений do_hardwork и check_hardwork для имитации занятости вычислительного ресурса просто засыпают на указанное количество миллисекунд. А после пробуждения отсылают менеджеру hardwork_done и hardwork_checked соответственно. Поскольку агенты засыпают на рабочих нитях диспетчера, то диспетчер не может их переиспользовать, пока агенты не проснуться. Т.е. выполняется такая дешевая имитация какой-то длительной работы (например, выполнение какого-то синхронного вызова, вроде обращения к БД или записи данных в сокет).

Пример позволяет выбрать, на каком диспетчере будут работать агенты. Можно указать либо one_thread, либо active_obj (каждый агент будет работать на своей собственной нити), либо thread_pool/adv_thread_pool. В случае thread_pool и adv_thread_pool агенты работают независимо друг от друга, в режиме individual FIFO. В случае active_obj диспетчера будет создано три рабочих нити (по количеству агентов). В случае thread_pool/adv_thread_pool будет создано столько рабочих нитей, сколько ядер определят C++ный рантайм (посредством std::thread::hardware_concurrency).

Соответственно, можно увидеть, как на суммарном времени работы теста сказывается возможность задействовать для обработки разное количество потоков. При использовании one_thread-диспетчера время получается строго линейным (т.е. количество запросов умноженное на величину тайм-аута и еще умноженное на два, т.к. тайм-ауты отсчитывают и агент-исполнитель, и агент-контроллер). У меня при параметрах 200 запросов и 20-мс тайм-ауте время составляет около 8 секунд.

Диспетчеры active_obj и thread_pool на 4-х ядерной машине дают время порядка 4 секунд. Т.е. за счет параллельной обработки запросов do_hardwork и check_hardwork получается просто результат умножения количества запросов на время выполнения одного запроса.

А вот adv_thread_pool на 4-х ядерной машине позволяет запустить 4-ре рабочих потока и распределить обработку всех do_hardwork и check_hardwork запросов между всеми рабочими потоками. Что сокращает время обработки теста до 2-х секунд.

Что хотелось бы отметить: сами агенты в тесте вообще не знают, на каких именно диспетчерах они будут работать. Ничего специализированного и заточенного под конкретный диспетчер в их коде нет. Разве что агенты исполнитель и контроллер указывают, что свои сообщения они обрабатывают в thread-safe методах. Что и позволяет получить эффект от распараллеливания на adv_thread_pool диспетчере.

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

четверг, 24 июля 2014 г.

[prog.c++] Задышал новый диспетчер в SObjectizer: агент может работать сразу на нескольких нитях

Сегодня в SObjectizer-5.4.0 начал работать еще один диспетчер, под условным названием adv_thread_pool. Он позволяет параллельно запустить несколько событий агента на разных нитях.

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

Было в мой практике несколько агентов, которые, по сути, являлись stateless преобразователями трафика. Т.е. они получали сообщение, как-то преобразовывали его, выбирали следующего адресата и отсылали сообщение дальше. Один из таких агентов, работая в самом нагруженном на тот момент приложении, свободно прокачивал через себя порядка 7-8 тысяч прикладных сообщений в секунду (и это на SO-4, который работает медленнее SO-5). Запас по производительности еще был, но ради одной амбициозной задачи могло потребоваться увеличить пропускную способность этого агента в 4-5 раз. Даже не смотря на то, что агент был написан на C++ и работал весьма эффективно, при такой нагрузке явно можно было бы упереться в потолок производительности на одной нити. Поэтому естественным решением выглядело бы распараллеливание обработки трафика на несколько ядер процессора. Т.е. нужно было научить агента распараллеливать свою работу.

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

Вот из таких соображений в версии 5.4.0 и появился adv_thread_pool-диспетчер. Т.е. продвинутый диспетчер с пулом рабочих потоков. Его продвинутость заключается в том, что он может определить, какие события каких агентов можно запустить одновременно на нескольких нитях, а для каких агентов этого делать нельзя. Так что, если агент умеет работать сразу на нескольких нитях, то будучи привязанным к adv_thread_pool-диспетчеру агент получит распараллеливание и балансировку нагрузки между несколькими рабочими потоками от SObjectizer-а.

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

Возможность распараллеливания агент задает на уровне своих событий оформляя подписку на сообщения. Если он подписывается обычным способом:

so_subscribe(mbox).event(&router::evt_reconfigure);

То такой обработчик будет считаться однопоточным (not thread safe в терминологии SObjectizer). И SObjectizer гарантирует, что к какому штатному диспетчеру не был привязан агент router, его событие evt_reconfigure будет запущено только на одной нити, и пока оно не завершится, никакие другие события агента запущены не будут.

Но, если пользователь передаст в event() еще один аргумент:

so_subscribe(mbox).event(&router::evt_route_message, so_5::thread_safe);

То SObjectizer поймет, что такой обработчик является безопасным для параллельного запуска на нескольких нитях (thread safe), и эта возможность будет задействована на тех диспетчерах, которые это поддерживают. Т.е. сейчас это adv_thread_pool, может со временем еще что-то добавится. Остальные же диспетчеры, которые предоставляют агенту всего лишь одну рабочую нить, не будут делать различия между thread safe и not thread safe обработчиками.

У такого подхода оказалось очень важное преимущество. Агент может сочетать thread safe и not thread safe обработчики. Например, обработчик сообщения о переконфигурировании агента объявляется однопоточным. И тогда adv_thread_pool сам обеспечит, чтобы все текущие обработчики завершились перед запуском обработчика evt_reconfigure. А когда evt_reconfigure завершит свою работу, adv_thread_pool сможет запустить несколько обработчиков evt_route_message сразу впараллель на нескольких рабочих нитях. Т.е. и здесь разработчик получает помощь от SObjectizer для обеспечения целостности агента в многопоточном окружении.

На данный момент у adv_thread_pool-а есть та же детская болезнь, что и у thread_pool-диспетчера: очень простая реализация disp_queue, из-за которой распределение заявок по нескольким рабочим нитям обходится дороже, чем этого хотелось бы. Поэтому мне предстоит погрузиться в изучение различных механизмов организации multi-producer/multi-consumer очередей. Правда, у SObjectizer есть одна важная особенность: когда есть работа, он должен разбираться с ней с наименьшими накладными расходами, но когда работы нет, SObjectizer должен спать не не жрать вычислительные ресурсы. Так что активное ожидание на spinlock-ах или атомиках в чистом виде не пройдет, нужно как-то задействовать и тяжелые примитивы синхронизации, вроде mutex-ов и condition_variable. Для multi-producer/single-consumer сценария такое решение нашлось и воплотилось в реальный код. Нужно теперь сварганить что-то подобное для MPMC-варианта. Так что буду изучать, сравнивать, думать, пробовать.

После чего, по большому счету, в версии 5.4.0 останется всего одно важное нововведение. Но, к сожалению, пока толком не проработанное. Хотя лично у меня есть ощущение, что что-то подобное уже давно нужно внедрить в SObjectizer и далее смотреть, что из этого получается. Речь идет о проблеме защиты агентов от перегрузки (overload control, если по-умному). Сейчас никакой защиты на уровне SObjectizer нет. Если разработчик ошибается в своих предположениях и агент не успевает разгребать то, что к нему валится, то очереди неограниченно пухнут, память отжирается, производительность падает, память может уйти в своп и т.д. На практике, к счастью, такое встречалось считанные разы за все 12 лет существования SObjectizer-4. Но, если позиционировать SObjectizer как серьезный рабочий инструмент, то что-то в этом направлении делать нужно. Некоторые предварительные мысли на сей счет есть, но далеко не уверен, что в реализацию пойдет именно такой вариант. Если кому-то эта тема интересно, то подключайтесь к обсуждению.

Ну вот как-то так. Работы много, едва-едва разгребаюсь. Когда будет релиз 5.4.0 точно не известно, как только, так сразу. Хорошо бы в августе, но тут уж как получится.

среда, 23 июля 2014 г.

[prog.management] И еще раз на тему KPI. Или если shit уже happened, то не поздно ли об этом узнавать сейчас?

Моя бывшая коллега, Марина Коледа (одна из самых больших потерь Интервэйла в 2014-м), на LinkedIn дала ссылку на еще одну статью про KPI для разработчиков софта: KPIs are for the Lazy. Статья на английском, не очень большая, читается легко. Для себя ничего нового я там не нашел, но в качестве еще одного аргумента против внедрения KPI для программистов вполне сойдет.

Однако, читая эту статью про KPI, я подумал об еще одной проблеме KPI, и не только KPI, но и вообще систем формальных показателей, важность которой я осознал только читая книги Генри Минцберга о менеджменте. А именно: любые результаты анализа или оценки формальных показателей могут показать вам уже случившиеся проблемы. И, зачастую, это уже слишком поздно. Грубо говоря, когда руководитель компании видит катастрофическое падение прибыли по итогам квартала (полугодия, года), то времени на исправление уже может и не быть (тут в тему мой недавний пост про Рона Джонсона в JC Penney).

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

Соответственно, чем больше времени прошло с момента принятия решения до получения "объективных доказательств" сейчас, тем меньше возможностей для исправления уже возникших проблем. Этот фактор "лага по времени" прекрасно осознается теми, кто внедряет KPI. Но устраняют его последствия они очевидным, простым, но неправильным способом: уменьшением лага, а не поиском других средств и способов диагностирования и устранения проблем на самой ранней стадии.

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

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

Да, становится возможным узнать, что некий Вася Иванов перестал крутить гайку №6 ключом на 16 в середине дня в среду. После чего два дня откручивал гайку №7 ключом на 15 и потратил на это на час больше, чем планировалось. Именно такая информация становится важной. Хотя акцент нужно было бы делать совсем на других вещах. Например, а почему гайку №6 крутил Вася Иванов, а не Ваня Федоров? И почему гайку №7 начали откручивать только после того, как закрутили гайку №6? И зачем вообще потребовалось крутить эти гайки?

Т.е. внедряя подробные KPI и имея возможность "по щелчку" видеть "актуальные отчеты о текущем положении дел" руководство начинает пребывать в уверенности, что обладает хорошим диагностическим инструментом. Который, действительно, хорошо выражает тяжесть случившейся проблемы в количественном выражении. Но на самом-то деле, нужна возможность определять симптоматику на самых-самых ранних стадиях. В общем, как в каком-то голливудском фильме: "Вы дали ему то, что он просил. Я же дал ему то, что ему было нужно".

Если кто-то из читателей считает, что вещи, которые я противопоставляю, на самом деле не противоречат друг другу, и можно в разработке ПО отдельно разруливать смысл работы (т.е. нужно ли вообще крутить гайку №6) и ее эффективность* то прошу поделиться опытом, где и как такого удалось достичь и как долго сие благолепие продолжалось. Мне же почему-то вспоминается один специалист по управлению проектами, выходец из большой компании с отлаженными рабочими процессами, просравшей при этом все что можно было просрать и переставшей в итоге существовать, который с большим удивлением у меня спросил: "Как, а вы разве в конце рабочего дня не прашиваете своих разработчиков о том, что они успели сделать за день?" К сожалению, я был тогда настолько шокирован этим вопросом, что не смог ответить, что мы до тех пор все делали качественно и в срок, пока у нас не стали спрашивать, а что же мы успели сделать за день :(

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


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

[prog.c++] Небольшая ошибка у Страуструпа в C++11 FAQ

Продолжая штудировать C++11 FAQ Бьёрна Страуструпа обнаружил маленькую ошибку в двух примерах кода:

Это в разделе "Algorithms improvements".

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

Именно потому, что в этом в принципе нет смысла, в std::unique_ptr конструктор и оператор копирования, получающие в качестве аргумента константную ссылку на std::unique_ptr, помечены как delete, т.е. запрещены к использованию. Именно поэтому, если у нас есть:

void f(std::unique_ptr<Big> a) {...}

Мы не можем написать так:

std::unique_ptr<Big> b(...);
f(b);

А должны писать одним из следующих способов:

f(std::move(b)); // Явное преобразование к rvalue reference.
                 // После этой конструкции b уже ничего не контролирует,
                 // владение объектом Big передано аргументу функции f().

f(std::unique_ptr<Big>(new Big(...))); // Неявный rvalue reference.
                 // Для конструирования аргумента a функции f()
                 // вызывается move constructor для std::unique_ptr.

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

template<class P> struct Cmp { // compare *P values
  bool operator() (const P & a, const P & b) const { return *a<*b; }
 }

и

sort(vb.begin(),vb.end(),
  [](const unique_ptr<Big> & a, const unique_ptr<Big> & b) { return *a<*b; });

Под катом автономный пример, с которым можно поиграться самостоятельно.

понедельник, 21 июля 2014 г.

[prog.c++] Предварительная информация о новом thread_pool-диспетчере в SObjectizer

В рамках разработки SObjectizer 5.4.0 как-то буднично произошло совсем не рядовое событие: полку штатных диспетчеров, доступных пользователю "искаропки" прибыло :) Появился thread_pool-диспетчер, который запускает привязанных к нему агентов на пуле рабочих потоков. До сих пор пользователям было доступно всего три диспетчера, появившихся в SO-4 еще в 2002-2003-м годах, а именно:

  • самый простой диспетчер с одной рабочей нитью (one_thread-диспетчер). Все привязанные к нему агенты работают на одной общей рабочей нити. Если какой-то из агентов "подвиснет", то подвиснут и все остальные агенты на этой рабочей нити;
  • диспетчер с активными объектами (active_obj-диспетчер). Каждому привязанном к диспетчеру агенту выделяется своя собственная рабочая нить. Если агент зависает, то зависает только его нить, на других агентов это не влияет;
  • диспетчер с активными группами (active_group-диспетчер). Рабочая нить выделяется группе агентов. Соответственно, если один агент из группы подвиснет, то подвиснут и остальные члены этой группы. Но другие агенты, принадлежащие другим группам (как и принадлежащие другим диспетчерам), смогут продолжать свою работу на своих рабочих контекстах.

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

Релиз SO-5.4.0, по оптимистичным оценкам, состоится не раньше второй половины августа, поэтому "в мир" новый thread_pool-диспетчер выйдет еще не скоро. Но я попробую рассказать о нем подробнее уже сейчас, дабы получить возможность обсудить сделанное и, при необходимости и появлении более удачных идей/предложений, иметь время на переработку. Для заинтересовавшихся рассказ о thread_pool-диспетчере под катом.

воскресенье, 20 июля 2014 г.

[prog.thoughts] Чтобы писать надежные многопоточные приложения...

...с использованием таких низкоуровневых механизмов как нити, мутексы/семафоры/мониторы, не говоря уже про еще более низкоуровневые средства вроде атомиков и барьеров, нужно:

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

Ну и плюс к тому, нужно быть чрезмерно самоуверенным и крайне оптимистичным разработчиком, чтобы ввязываться в такие авантюры :)))