пятница, 8 сентября 2017 г.

[prog.c++] json_dto-0.2

Мы сегодня обновили свою небольшую библиотеку json_dto, которая служит хоть и тонкой, но очень полезной оберткой над такой замечательной штукой, как rapidjson. Мы сделали json_dto где-то года полтора назад, взяв идеи из Boost.Serialization, для уменьшения объема писанины при работе с JSON в C++. Например, json_dto позволяет писать вот так:

class User {
public :
   /* ... */

   template<typename JSON_IO>
   void json_io(JSON_IO & io) {
      io & json_dto::mandatory("id", _id)
         & json_dto::mandatory("name", _name)
         & json_dto::mandatory("birthday", _birthday)
         & json_dto::mandatory("phone", _phone);
   }
};

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

вторник, 5 сентября 2017 г.

[prog.thoughts] Сложные инструменты уже не нужны в современных условиях?

Размышляя время от времени над феноменом языка Go, над тем, что пишут на Хабре или обсуждают на профильных ресурсах (типа LOR-а или RSDN-а), закрадывается мысль, что в современных условиях сложные инструменты мало кому нужны. И я не могу понять, это объективная реальность такова или же это я вошел в пору конфликта "отцов и детей", но уже со стороны "отцов".

Но вот взять тот же C++, который сейчас повсеместно ругают за сложность. Забывая при этом, что сложность эта возникла не просто так, а как адаптация языка к тем прикладным нишам, в которых он активно используется. Сложная система шаблонов в C++ появилась не просто так же. Специализация шаблонов, к примеру, является следствием того, что обобщенные структуры и/или алгоритмы бывает нужно адаптировать к конкретным условиям. Ну это же объективная реальность такая: ты либо борешься со сложностью свой прикладной задачи посредством мощного инструмента, либо же тупо тратишь больше времени и усилий, обходясь намного более примитивными средствами. Скажем, там, где можно сделать один шаблон и применить его для 5 разнотипных наборов данных, можно же обойтись и без шаблона, посредством копипасты.

Мне часто вспоминается первое знакомство с C++ в далеком 1991-ом году. Язык был гораздо сложнее Паскаля и Бейсика. Но зато он позволял сделать свои классы Set и Matrix, которые бы ничем не отличались бы от встроенных в язык типов. А уж когда в C++ завезли шаблоны, то тут вообще такие бескрайние просторы открылись, что просто дух захватывало. Можно было сделать Matrix<T>, где T мог быть, скажем, Complex<U>, да и сам U мог быть не просто float-ом или double, а каким-то собственным FixedSizeFloat...

Конечно, C++ здесь не очень показательный пример, т.к. за его мощность нужно было платить унаследованными от C граблями. Но можно посмотреть и на другие знаковые языки 1980-х и 1990-х годов. Ada, например. Или Eiffel. Да взять ту же Java, которая вышла в свет в 1995-ом как очень примитивный язык. Который был вынужден со временем усложниться и заиметь таки генерики. С C# затем это так же произошло, но гораздо быстрее.

Т.е. в 1980-х и 1990-х, да даже в начале 2000-х, растущая сложность инструментария воспринималась как само собой разумеющееся. Думаю, что это происходило потому, что программистов было мало, сложность программ росла очень быстро, потребность в софте росла еще быстрее. Получалось, что когда программистов мало, а задача сложная, то решать ее методом грубой силы не получится, просто нет ресурсов. Значит решать ее можно было посредством более мощных, а значит и более сложных, инструментов.

Но потом что-то изменилось. Возможно, двумя последними языками из знакомых мне, которые пошли по пути создания сложного, но мощного инструмента, были D и Scala. А вот то, что стало появляться затем, образует уже иную тенденцию. Go -- это вообще какой-то крайний случай. А вот языки, вроде, Ceylon, Kotlin и даже Rust, как мне кажется, идут по пути снижения сложности, но при этом предоставления пользователям достаточной мощности. Хотя "достаточной" -- это относительное понятие. Например, нет в Rust-е нормального ООП и кому-то Rust может казаться достаточно мощным, а кому-то -- нет.

Думается, что все это таки объективно. Подавляющему большинству разработчиков сейчас не нужны сложные инструменты. Ибо изменился "ландшафт" программирования. Сложных задач в процентном отношении становится меньше. Больше становится рутины, в которой основная сложность не в самом программировании, а в организации процесса разработки: от общения с заказчиком и формализации требований до интеграционного тестирования и запуска в эксплуатацию. Программист -- это винтик, который должен быть быстрообучаемым и легкозаменяемым. Чего сложно достигнуть, если программист будет использовать C++ или Scala, а не Go или Kotlin.

пятница, 1 сентября 2017 г.

[prog.c++] RESTinio в конкурсе HighLoad Cup-2017 от Mail.ru

Мы у себя в "СтифСтриме" занимаемся разработкой небольшого C++ного фреймворка RESTinio (последняя публичная версия лежит здесь). Цель в том, чтобы дать C++ разработчикам легковесный, простой в использовании, но мощный и производительный инструмент для разработки RESTful сервисов на C++. А тут Mail.ru объявляет конкурс HighLoad Cup. Естественно, захотелось попробовать RESTinio в чужом бенчмарке, в котором мы ничего не контролируем. В общем, Коля Гродзицкий, который отвечает за разработку RESTinio, и занялся подготовкой конкурсного решения для HighLoad Cup-а.

В качестве технологического стека использовался C++14/17 (в объеме, поддерживавшемся в gcc-6.2), RESTinio, Asio, Node.js http-parser, RapidJSON и json_dto. Отдельного экстрима доставило то, что в этот же момент в RESTinio вливалется поддержка WebSocket-ов. Так что решение приходилось делать на не стабильной версии RESTinio, нестабильность которой увеличивалась за счет выявленных в процесс работы над конкурсным решением недостатков самого RESTinio. В общем, Николаю досталось :)

В итоге Колино решение вошло в финал с 45-го места. И в финале оно оказалось на 44-ом месте (на 2017.09.04).

С учетом того, что в финал, как мне представляется, вошло всего несколько решений, использующих готовые, более-менее полноценные реализации HTTP-серверов, не заточенных под эту конкретную задачу, результат не самый плохой. Как раз рядом с Колиным решением оказалось еще пару решений на Go и fasthttp (а производительность у RESTinio и fasthttp была очень близка, когда мы такие замеры производили в последний раз). Вероятно, четвертый-пятый десяток ТОПа -- это максимум, на который могут претендовать решения, построенные на универсальных инструментах и не оптимизированные на самом низком уровне под конкретные условия.

Но для нас более важным оказалось другое: RESTinio получило первое более-менее серьезное боевое крещение. И мы сами обнаружили целый ряд мест, в которых нужно не только активно дорабатывать напильником, но и вообще довольно глубоко копать в будущих версиях, чтобы дать пользователям RESTinio удобный и настраиваемый под их нужды инструмент. Тем более, что выбор таковых под C++ совсем невелик. В этом плане участие в конкурсе оправдало себя на все 146%.

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


От себя лично добавлю, что я не понял логики организаторов турнира в финале. Как по мне, так нужно было либо делать один рейтинговый обстрел (но объемом в 4-5 раз больше, чем в отборочном туре, чтобы нивелировать разные факторы вроде фазы Луны), либо же делать N обстрелов с одинаковыми данными и высчитывать среднее, либо делать N обстрелов с разными данными и суммировать время. А так получается N попыток и в зачет идет лучшее. Что выглядит особенно непонятно в ситуациях, когда какое-то из решений на отдельных обстрелах уходит по времени в бесконечность или вообще падает.

Очевидно, что Колиному решению на RESTinio это бы никак не помогло. Но зато не было бы вопросов по поводу отдельных решений из ТОП-10, которые в некоторых обстрелах показывают времена в районе 150K и более секунд. Или вообще падают. Но находятся на верхних строчках.

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

[life.cinema] Очередной кинообзор (2017/08)

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

Мегрэ расставляет сети (Maigret Sets a Trap, 2016), Мертвец детектива Мегрэ (Maigret's Dead Man, 2016) и Мегрэ: Ночь на перекрёстке (Maigret: Night at the Crossroads, 2017). Вот все хорошо. От картинки так вообще получаешь огромное удовольствие. Но к чему невозможно привыкнуть, так это к худому комиссару Мегрэ. Как въелось с детства, что Мегрэ был грузным и боролся с перееданием, так и не отпускает до сих пор :)

Тайна 7 сестер (Seven Sisters, 2017). Если не придираться к сюжету, то очень даже неплохо. Руми Рапас, сыгравшая сразу семерых персонажей, вызывает уважение.

Дикая история (El bar, 2016). Обажаю такие, по хорошему укуренные, истории. Поэтому мне очень понравилось. Но фильм для ценителей жанра.

Валериан и город тысячи планет (Valerian and the City of a Thousand Planets, 2017). Очень красочный и очень-очень детский фильм. Рассчитан на аудиторию 8-10 лет, как мне показалось. Так что взрослым можно смотреть только за компанию с детьми.

Послание от Кинга (Message from the King, 2016). Неплохо. Не шедевр, но неплохо. Нормальный такой криминальный фильм без претензий на глобальность и масштабность.

Телохранитель киллера (The Hitman's Bodyguard, 2017). Как по мне, так трейлеры фильма оказались круче, чем сам фильм. Экшен-сцены хороши, но все, что между ними, навевает смертельную скуку. Ну и для фильма такого уровня неожиданно было увидеть не очень уж качественно нарисованные на компьютеры взрывы. Так что фильм несколько разочаровал.

Тёмная башня (The Dart Tower, 2017). Оригинальные произведения Стивена Кинга я не читал, так что для меня это все совершенно новая история. Как по мне, так слишком простенько и недорого. Идрис Эльба и Мэттью МакКонахи, конечно, хороши. Но их не хватает, чтобы сказать, что получилось крутое и зрелищное кино.

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

Стена (The Wall, 2017). Противоречивые ощущение. Сначала история захватывает. Потом становится скучно. Потом главный герой начинает подбешивать. Ближе к концу в фильме почти что разочаровываешься. Но финал откровенно доставляет.

Первое убийство (First Kill, 2017). Как-то ни о чем. Можно и не смотреть.

четверг, 24 августа 2017 г.

[business.book] Дочитал книгу Константина Бакшта "Как загубить собственный бизнес. Вредные советы предпринимателям"

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

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

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

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

Однако, подобные недостатки не отменяют того факта, что книга явно относится к категории must read.

Точнее так: тем, кого устраивает роль наемного работника, данная книга ни к чему. А вот тем, кто задумывается над открытием своего дела (какие бы побуждения за этим не стояли), прочесть ее нужно обязательно. Желательно перед открытием своего дела. Но можно и после :)

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

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

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

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

суббота, 19 августа 2017 г.

[prog.c++] Небольшое обновление библиотечки cpp_util

Есть у нас маленькая библиотека cpp_util, которая является небольшой коллекцией всяких мелких полезностей (часть из которых с развитием C++ теряет актуальность, но все-таки). Время от времени мы туда какие-то полезные мелочи добавляем.

Давеча была добавлена вспомогательная функция terminate_if_throws. Эта функция получает и вызывает переданную ей лямбда-функцию. Если лямбда бросает исключение, то автоматически вызывается std::terminate (поскольку сама terminate_if_throws помечена как noexcept).

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

Ну, например, допустим у нас есть какой-то архисложный класс some_complex_data и у него есть метод do_some_modification, который меняет состояние объекта. Мы хотим, чтобы do_some_modification обеспечивал строгую гарантию безопасности исключений: т.е. либо все изменения происходят успешно, либо нет никаких изменений.

Для этого в реализации do_some_modification будет, грубо говоря, три основных шага:

  1. Проверка всех необходимых условий. Если какое-то условие не выполняется, то может быть брошено исключение.
  2. Преаллоцация необходимых ресурсов для выполнения операции. Тут запросто может выскочить исключение, если каких-то ресурсов нет.
  3. Окончательная фиксация изменений.

Достаточно тяжело написать do_some_modification() так, чтобы выжить при возникновении исключений на третьем шаге. Гораздо проще, а потому и надежнее, сделать так, чтобы при возникновении исключения на третьем шаге тупо вызывать std::abort()/std::terminate(). Как раз для этого и предназначена terminate_if_throws: она явным образом выставляет в коде метку о том, что вот здесь мы никаких исключений не ждем в принципе, а если исключение таки случится, то пережить это мы не сможем:

#include <cpp_util_3/terminate_if_throws.hpp>
...
// We want to provide strong exception guarantee for that method.
void some_complex_data::do_some_modification(const params & p) {
  // Checks all necessary conditions first.
  // Some exceptions can be thrown here.
  check_condition_one(p);
  check_condition_two(p);
  ...
  // Preallocate some resources.
  // Exceptions are expected here. But this is not a problem
  // because there is no any actual state changes yet.
  auto r1 = preallocate_resource_one(p);
  auto r2 = preallocate_resource_two(p);
  ...
  // All preparations are done. We don't expect exceptions
  // in the following block of code. But if some exception is thrown
  // then we don't know how to repair from it.
  cpp_util_3::terminate_if_throws( [&] {
    do_state_change_action_one(...);
    do_state_change_action_two(...);
    ...
  } );
}

Так же в cpp_util был добавлен макрос CPP_UTIL_3_UNIX. Сейчас он определяется в cpp_util_3/detect_compiler.hpp если определен один из символов: unix, __unix или __unix__.

PS. В cpp_util вряд ли есть что-то уникальное. Наверняка в больших библиотеках, вроде Boost-а или Folly есть соответствующие аналоги. Но смысл cpp_util был как раз в том, чтобы в мелкие проекты не тянуть тяжелые зависимости масштаба Boost-а или Folly.

среда, 16 августа 2017 г.

[prog.bugs] Сделал, нашел и исправил любопытный баг в многопоточном коде :)

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

Сценарий приблизительно такой:

  • нить №1 создает объект env;
  • на контексте нити №1 у объекта env вызывается метод start(). Внутри env.start() запускается цикл обработки событий Asio (т.е. вызывается asio::io_service::run()). По сути, env.start() вернет управление только когда завершится работа asio::io_service::run();
  • в одном из событий на контексте нити №1 создается нить №2. Ссылка на объект env передается в нить №2;
  • нить №2 какое-то время выполняет свои действия, после чего вызывает env.stop(). Внутри stop-а дается команда завершить цикл обработки событий Asio. Точнее говоря, внутри env.stop() выполняется ряд действий, одно из последних в котором -- это вызов asio::io_service::stop();
  • сразу после вызова env.stop() нить №2 завершает свою работу;
  • когда на нити №1 завершается env.start(), нить №1 разрушает объект env и дожидается завершения работы нити №2;
  • когда нить №2 завершается, завершается и работа нити №1.

Все это работало на реальном железе под Windows и gcc-5.2/vc-15.3. Но вот под Linux-ом внутри виртуалки начало падать. Не всегда, но довольно-таки регулярно.

Падало где-то между вызовом env.stop() на контексте нити №2 и сразу после возврата из env.start() на нити №1. Т.е. падало стабильно внутри нити №2 при вызове env.stop(), а нить №1 только что возвращалась из env.start().

Сразу стало очевидно, что это баг. Спустя какое-то время стало понятно, что баг происходит из-за того, что в нити №1 происходит возврат из env.start() и уничтожение env. А нить №2 все еще находится внутри env.stop(). Оставалось понять, как же так происходит, что ссылка на env внутри нити №2 перестает быть валидной прямо внутри вызова env.stop(), ведь вызов asio::io_service::stop() выполняется в самом конце и после этого вызова внутри env.stop() уже ничего не делается.

Метод env.stop() выполнял следующие шаги:

  • захватывал замок объекта env;
  • проверял, запустил ли кто-нибудь процедуру shutdown;
  • если процедура shutdown еще не запущена, то:
    • выставлял признак запуска процедуры shutdown;
    • освобождал замок объекта env;
    • выполнял ряд действий по освобождению выделенных ресурсов (эти действия должны были выполняться при освобожденном захвате объекта env);
    • вновь захватывал замок объекта;
    • проверял, все ли ресурсы освобождены (освобождение может выполниться сразу, а может занять какое-то время). Если все ресурсы освобождены, то вызывал asio::io_service::stop(). Если не все ресурсы освобождены, то просто завершал свою работу, т.к. после освобождения последнего ресурса env.stop() вызвал бы кто-то другой;
  • если же процедура shutdown была запущена, то:
    • проверял, все ли ресурсы освобождены (освобождение может выполниться сразу, а может занять какое-то время). Если все ресурсы освобождены, то вызывал asio::io_service::stop()
  • освобождал замок объекта env.

Проблема оказалась вот в чем: когда нить №2 начинает освобождать ресурсы, то все ресурсы могут быть освобождены сразу же. Как только это случается, просыпается нить №1, которая сама дергает stop() на своем контексте. Когда stop() вызывается на нити №1, то обнаруживается, что процедура shutdown запущена, все ресурсы освобождены. Поэтому вызывается asio::io_service::stop(), это приводит к возврату из asio::io_service::run(), а следом и к возврату из env.start(). А значит и к разрушению env.

Но в это время нить №2 все еще внутри env.stop(). Она как раз завершила освобождение всех ресурсов и пытается вновь захватить замок объекта env. Но к этому моменту объекта env уже нет, а значит и нет его замка. Поэтому тут-то и и возникает сегфолт.

В общем-то, ничего особенного. Нить №1 контролирует время жизни объекта env, а нить №2 пользуется этим объектом, не имея возможности как-то повлиять на время его жизни. Поэтому-то когда нить №1 уничтожает объект env, у нити №2 остается повисшая ссылка.

Любопытным этот баг делает то, что я почему-то посчитал, что метод env.stop() будет являться атомарным. Что на самом деле оказалось не так. Внутри env.stop() было "вложенное" освобождение и повторный захват замка объекта env. Как раз это вложенное освобождение и позволило нити №1 вклиниться в работу и совершить свои черные деяния. При этом вероятность того, что нить №1 окажется свободной от каких-то своих действий для того, чтобы сразу же среагировать на освобождение всех ресурсов, да так быстро, что нить №2 не успеет повторно захватить замок объекта, была очень низка. Что и показывали успешно проходившие под Windows тесты. Но вот под Linux-ом в виртуалке эта вероятность материализовалась. Причем достаточно стабильно. Так что тут мне изрядно повезло.

Посему повторюсь: многопоточное программирование на голых нитях и мутексах -- это пот, боль и кровь сложно. Не нужно такими вещами заниматься. Оставьте это занятие опытным мазохистам ;)