вторник, 1 января 2030 г.

О блоге

Более двадцати лет я занимался разработкой ПО, в основном как программист и тим-лид, а в 2012-2014гг как руководитель департамента разработки и внедрения ПО в компании Интервэйл (подробнее на LinkedIn). В настоящее время занимаюсь развитием компании по разработке ПО stiffstream, в которой являюсь одним из соучредителей. Поэтому в моем блоге много заметок о работе, в частности о программировании и компьютерах, а так же об управлении.

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

понедельник, 31 декабря 2029 г.

[life.photo] Характерный портрет: вы и ваш мир моими глазами. Безвозмездно :)

Вы художник? Бармен или музыкант? Или, может быть, коллекционер? Плотник или столяр? Кузнец или слесарь? Владеете маленьким магазинчиком или управляете большим производством? Реставрируете старинные часы или просто починяете примус? Всю жизнь занимаетесь своим любимым делом и хотели бы иметь фото на память?

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

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

[work] Открыт для сотрудничества в качестве C++ разработчика

В виде (суб)контракта с нашей компанией СтифСтрим.

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

Самостоятельно погружаюсь в проблему, нахожу решение, кодирую, тестирую, документирую. Если нужно обучаю. Если нужно сопровождаю и поддерживаю. Если нужно выступаю в качестве евангелиста (см. список публикаций на Хабре).

Работаю не быстро, но качественно, беру недорого.

Оценить мой уровень можно, например, про проекту aragata, реализованному мной практически в одиночку. Код можно увидеть на GitHub-е, на Хабре есть две статьи о том, что это и как работает: вводная статья и описание сделанных по результатам нагрузочных испытаний оптимизаций + вот этот пост.

В качестве дополнительных примеров: timertt (+ документация), so5extra (+ документация) -- эти проекты так же написанные мной самостоятельно.

Связаться со мной можно через eao197 на gmail тчк com. Если кому-то интересен профиль на LinkedIn, то вот.


Это сообщение повисит какое-то время вверху. Потом будет видно, имеет ли смысл пытаться дальше оставаться в C++.

[prog.multithreading.bugs] Повезло столкнуться с собственным багом в многопоточном коде

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

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

Но, как оказалось, не всегда это выполнялось правильно. Даже не смотря на наличие тестов 🙁

Особо доставили два момента:

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

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

Так что внезапно обнаружил себя в ситуации, когда баг явно есть, но как он возник решительно непонятно. Как и непонятно есть ли вообще возможность его воспроизвести (и во что все это выльется).

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

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

Оказалось вот что: у меня был ассоциативный контейнер (std::map), содержимое которого защищалось мутексом. Но в одной из веток происходило следующее:

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

По недосмотру в коде не оказалось повторного захвата мутекса после того, как текущая нить дождалась своего события и проснулась. Поэтому обновление контейнера было уже не thread-safe 🥴

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

В общем, целый ряд счастливых случайностей:

  • сперва я очень удачно сгенерировал "правильную" последовательность запросов которая привела к тому, что две рабочие нити проснулись в одно время;
  • затем повезло с тем, что при перезаписи std::map-а из разных потоков не образовался какой-то мусор из-за чего бы программа могла бы упасть с segmentation fault;
  • и все это случилось когда в программе еще оставались отладочные печати, благодаря которым на консоль сбрасывались дампы с информацией о текущих запросах;
  • ну и каким-то чудом в этих самых дампах я заметил то, что у ряда запросов статус оказался "в работе", а не "в ожидании".

Короче говоря, без везения в поиске багов в многопоточке не обойтись 😎

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

Сам я себя ни в коем случае специалистом по многопоточному программированию не считаю, мне тупо не хватает мозгов, чтобы моделировать все то многообразие сочетаний событий, которое может возникнуть в многопоточном коде. Я поэтому-то SObjectizer-ом и занимаюсь, чтобы свести работу с многопоточностью к минимуму. Поэтому в моем многопоточном коде баги были, есть и будут. Куда же без них 😉 Главное, чтобы они вовремя наружу вылазили, под присмотром 🤣


На правах саморекламы: изобретаю велосипеды для себя, могу изобретать и для вас.

суббота, 10 февраля 2024 г.

[prog.multithreading] Нужна помощь в поиске названия для примитива синхронизации, похожего на std::latch

Мне тут потребовался примитив синхронизации, в чем-то похожий на добавленный в C++20 std::latch. Но с важным отличием: в `std::latch` нужно в конструкторе указывать значение счетчика. А в моем случае это количество заранее точно неизвестно.

Грубо говоря, сценарий использования `std::latch`: есть тред A, который ждет, пока N тредов B(i) сделают кусок работы. Тред A засыпает на `wait`, каждый тред B(i) рано или поздно вызывает `count_down` и когда это сделают все треды B(i), тред А проснется.

Все это отлично работает пока N известно заранее.

В моем же случае тред С создает сперва тред A, а затем начинает создавать треды B. И тред A точно не знает, сколько именно C создаст тредов B. Просто в какой-то момент треду A нужно будет дождаться пока запущенные треды B завершат свою работу. Для чего каждый тред B сперва инкрементирует счетчик, а затем декрементирует. Треду же А достаточно дождаться обнуления этого счетчика.

Сделанный для этих целей простой вариант "барьера" можно увидеть под катом.

Используется приблизительно следующим образом:

// Это все внутри треда C.
meeting_room_t completion_room;

std::thread thread_a{[&]() {
  ... // что-то делает.
  // Нужно дождаться пока треды B завершат свою работу.
  completion_room.wait_then_close();
  ... // еще что-то делает.
}};

// Создаем треды B.
std::vector<std::thread> threads_b;
while(some_condition()) {
  threads_b.push_back([&completion_room]() {
    completion_room.enter(); // Увеличили счетчик.
    ... // что-то делает.
    completion_room.leave(); // Уменьшили счетчик.
  });
  ... // какие-то еще действия.
}

// Осталось дождаться завершения работы.
for(auto & t : threads_b) t.join();
thread_a.join();

Возникла сложность с названием для такого примитива синхронизации.

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

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

Upd. Похоже, что такая штука назвается rundown: Run-Down Protection. Большое спасибо Константину за наводку.

Текущая реализация meeting_room_t:

вторник, 6 февраля 2024 г.

[prog.c++.kill'em-all] C++ный код, от которого у меня изрядно подгорает

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

error_code use_resource(resource_id res_id) {
  if(auto r = first_operation(res_id); r != error_code::ok) {
    dispose(res_id);
    return r;
  }
  if(auto r = second_operation(res_id); r != error_code::ok) {
    dispose(res_id);
    return r;
  }
  ...
  dispose(res_id);
  return error_code::ok;
}

Думаю, что несложно догадаться, что именно триггерит: это обилие вызовов dispose.

Я могу простить тот факт, что в use_resource передается голый дескриптор ресурса, а не какая-то RAII-обертка вокруг него.

Ну мало ли, бывает. Может эта функция вообще как extern "C" описана и предназначена для того, чтобы ее вызывали из Си-шного кода. Или же это часть древнего проекта и первоначально use_resource была написана еще в конце 1980-х, а сейчас ее просто дорабатывают не имея возможности поменять все 100500 мест в старой кодовой базе, где она вызывается именно вот так.

Но блин, почему нельзя сделать RAII обертку уже внутри use_resource?

Хотя бы подобным образом:

error_code use_resource(resource_id res_id) {
  struct resource_disposer {
    resource_id m_id;
    resource_disposer(resource_id id) : m_id(id) {}
    ~resource_disposer() { dispose(m_id); }
  } disposer(res_id);

  if(auto r = first_operation(res_id); r != error_code::ok) {
    return r;
  }
  if(auto r = second_operation(res_id); r != error_code::ok) {
    return r;
  }
  ...
  return error_code::ok;
}

Причем реализация такого `resource_disposer` -- это вообще C++98. Таким подходом можно пользоваться уже больше двадцати пяти(!!!) лет без оглядки на версию компилятора. В современном C++ можно было бы найти еще несколько способов достижения той же самой цели (хотя бы finally из GSL), более лаконичных.

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

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

Вероятно, C++ программистов нужно начинать учить с идиомы RAII. А уже все остальное -- потом.

Ну а Си-программистов, по аналогии, нужно начинать учить с идиомы goto err (или goto cleanup). Даже не смотря на то, что goto -- это зло. Как и чистый Си, впрочем ;)


На правах саморекламы: изобретаю велосипеды для себя, могу изобретать и для вас.

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

[prog.c++] Захотелось в C++ странного (на тему транзитивной константности)...

Недавно столкнулся с задачей, в которой было бы хорошо иметь транзитивную константность в C++.

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

Что такое транзитивная константность?

Представим себе, что у нас есть:

class Foo {
public:
  void foo(); // Это не-const метод.
  void foo2() const// А это уже const-метод.
};

class Bar {
  Foo * m_foo; // Это не-const указатель!
public:
  ...
  void bar2() const { // Это const-метод, в котором мы не можем присвоить m_foo новое значение.
    m_foo->foo(); // Но зато можем вызвать не-const метод для m_foo.
  }
};

Foo foo;
const Bar bar{&foo};
bar.bar2(); // Этот вызов может изменить foo.

Из-за того, что в C++ константность не транзитивна, то в const-объекте bar можно иметь не-const указатель на foo и в const-методе Bar::bar2 можно поменять объект foo.

Если бы константность была транзитивной, то в Bar::bar2 указатель Bar::m_foo автоматически бы стал константным и вызвать в Bar::bar2 не-const метод Foo::foo у нас уже не получилось бы.

Поскольку в С++ транзитивной константности нет, то я было попробовал сделать ее вручную. По типу чего-то такого:

template<typename T>
class AutoConstPtr {
  T * m_ptr;
public:
  ...
  [[nodiscard]] T * get() { return m_ptr; } // Не-const.
  [[nodiscard]] const T * get() const { return m_ptr; } // Уже const.
};

Это позволяет получить транзитивную константность в простом случае:

class Bar {
  AutoConstPtr<Foo> m_foo; // Это уже не raw pointer.
public:
  ...
  void bar2() const {
    m_foo.get()->foo(); // А вот здесь будет ошибка компиляции!
  }
};

И это уже было именно то, что мне нужно. И, казалось бы, счастье было уже так близко...

Но, к сожалению, это не сработало на практике. Например, из-за вот таких случаев:

void ProcessItems(const std::vector<AutoConstPtr<Foo>> & items) {
  for(auto p : items) {
    p.get()->foo(); // Упс!
  }
}

Фокус в том, что p -- это будет копия AutoConstPtr<Foo>. Не-const копия. Следовательно, для p будет вызываться не-const версия get. Следовательно, будет возвращаться не-const указатель на Foo. Следовательно, можно вызывать не-const методы Foo, т.е. модифицировать Foo. И это в ситуации, когда исходно у нас были как раз константные указатели на Foo (ведь у нас const-ссылка на вектор указателей).

Вот таким вот незамысловатым образом красивая идея накрылась медным тазом. Абыдна, да 🙁

И вот тут мне захотелось, чтобы в C++ при описании конструктора можно было бы явно описать, применяется ли этот конструктор для const-объекта или нет.

Ведь сейчас мы пишем что-то вроде:

MyClass::MyClass(const MyClass & other) {...}

и понимаем, что это конструктор копирования. Но не понимаем, какой именно экземпляр MyClass при этом конструируется. Т.е.:

MyClass source{...};

MyClass copy1{source}; // Вызов конструктора копирования.
const MyClass copy2{source}; // Вызов того же самого конструктора копирования.

А что, если бы мы могли добавлять const и к конструктору?

template<typename T>
class AutoConstPtr {
  T * m_ptr;
public:
  ... // Здесь какие-то другие конструкторы.
  AutoConstPtr(const AutoConstPtr &) = delete// Нельзя построить не-const из const.
  AutoConstPtr(const AutoConstPtr & other) const // Тут все OK.
    : m_ptr{other.m_ptr}
  {}
  ...
};

Тогда бы не получилось бы скомпилировать конструкцию:

for(auto p : items) ...

потому что нельзя построить не-const объект AutoConstPtr из const-объекта.

А вот так бы получилось бы:

for(const auto p : items) ...

Вот такая вот странная фантазия.

Но это реально фантазия, т.к. даже если бы была возможность помечать конструкторы как const, то непонятно было бы что делать вот с такими ситуациями:

const auto std::vector<AutoConstPtr<Foo>> & source = ...;
std::vector<AutoConstPtr<Foo>> selected;
std::copy_if(source.begin(), source.end(),
    std::back_inserter(selected),
    [](const auto & item) { return ...; });

Так что, возможно, идея транзитивной константности в принципе не для C++.