Показаны сообщения с ярлыком Multithreading. Показать все сообщения
Показаны сообщения с ярлыком Multithreading. Показать все сообщения

суббота, 25 июля 2026 г.

[prog.c++.multithreading] Сломал себе мозг пытаясь понять что не нравится thread sanitizer-у

Upd. Похоже, что проблема сперва была в том, что SO-5 подключался в проект через vcpkg и когда проект компилировался с TSan, то линковался SO-5, собранный без TSan. А когда я включил исходники SO-5 в сам проект, чтобы все компилировалось с одинаковыми ключами, то ошибся с порядком команд в CMakeLists.txt и при компиляции SO-5 опции для TSan не учитывались. Если же собрать и SO-5, и остальной проект с одинаковыми опциями, то данной проблемы не возникает (пока?).

В текущем проекте thread sanitizer периодически выдает предупреждение о data race на фрагменте, который относится к SObjectizer-у.

Самое плохое то, что:

  • я не понимаю в чем именно thread sanitizer видит проблему. Соответственно, неизвестно, является ли срабатывание TSan-а ложно позитивным или же есть реальная ошибка, которую следует исправить;
  • мне не удается повторить такую же ситуацию в тестах для самого SO-5. Т.е. в рамках проекта TSan диагностику выдает, а в мелких тестах, которые пытаются повторить тот же сценарий -- нет. Ни в какую. Что сильно затрудняет разбирательства и поиск обходных путей.

Что здесь происходит:

Агент на нити T7 отсылает сообщение GiveMeTask агенту-координатору, который работает на нити T3.

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

В это же время на нити T7 завершается процедура отсылки сообщения.

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

Экземпляр сообщения GiveMeTask создается на нити T7. Указатель на этот экземпляр хранится на нити T7 внутри объекта intrusive_ptr_t.

На нити T3 внутри execution_demand_t так же есть свой объект intrusive_ptr_t который хранит указатель на этот же экземпляр GiveMeTask.

Т.е. на двух нитях есть два разных intrusive_ptr_t, которые хранят в себе указатель на один и тот же объект GiveMeTask.

При этом счетчик ссылок на GiveMeTask хранится в самом объекте GiveMeTask. Класс GiveMeTask наследуется от so_5::message_t:

struct GiveMeTask final : public so_5::message_t
{
    const so_5::mbox_t _workerMbox;

    GiveMeTask(so_5::mbox_t workerMbox)
        : _workerMbox{ std::move(workerMbox) }
    {}
};

А so_5::message_t наследуется от so_5::atomic_refcounted_t:

class message_t : public atomic_refcounted_t
    {
        ...
    };

В so_5::atomic_refcounted_t счетчик ссылок хранится в виде std::atomic. Т.е. операции инкремента-декремента количества ссылок происходят атомарно и не нуждаются в дополнительной синхронизации.

Получается, что на нити T7 создается новый экземпляр GiveMeTask, указатель на него сохраняется в локальном объекте intrusive_ptr_t и счетчик ссылок на GiveMeTask выставляется в 1.

На нити T7 вызывается send для GiveMeTask и формируется execution_demand_t для агента-координатора. Внутри execution_demand_t создается свой intrusive_ptr_t и счетчик ссылок для GiveMeTask получает значение 2.

Затем на нити T3 происходит обработка GiveMeTask, после чего начинается разрушение execution_demand_t и его содержимого (в том числе и второго intrusive_ptr_t).

Но чуть раньше на нити T7 происходит разрушение своего intrusive_ptr_t после чего счетчик ссылок в GiveMeTask опускается до 1.

А уже после этого на нити T3 счетчик ссылок на GiveMeTask обнуляется и происходит разрушение объекта GiveMeTask.

Происходят действия именно в этом порядке. Если бы сперва полностью разрушился execution_demand_t на нити T3 и лишь после этого началось уничтожение intrusive_ptr_t на нити T7, то деструктор GiveMeTask вызвался бы на нити T7, а не на нити T3.

Т.е. с моей точки зрения здесь все OK. Но TSan видит data race. А я не понимаю про какой data race идет речь.

Под катом выхлоп от TSan в текстовом виде.

пятница, 17 июля 2026 г.

[prog.c++.multithreading] Мой способ обойти ложное срабатывание в TSan с инверсией порядка захвата mutex-ов

Продолжение вчерашней темы с ложно позитивным срабатыванием thread sanitizer, когда TSan ошибочно диагностировал инверсию порядка захвата mutex-ов.

Поскольку в коде с точки зрения порядка блокировок все было OK, то возник вопрос: а как удовлетворить TSan, чтобы избавиться от ложной диагностики и продолжить использовать TSan для поиска других проблем?

Было найдено вот такое решение:

#if defined( __SANITIZE_THREAD__ )

templatetypename M >
class tsan_friendly_lock_guard
{
   M & m_what;

public:
   tsan_friendly_lock_guard( M & what )
      : m_what{ what }
   {
      while( !m_what.try_lock() )
      {
         std::this_thread::yield();
      }
   }

   ~tsan_friendly_lock_guard()
   {
      m_what.unlock();
   }
};

#else

templatetypename M >
class tsan_friendly_lock_guard
   {
      std::lock_guard< M > m_guard;

   public:
      tsan_friendly_lock_guard( M & mutex )
         : m_guard{ mutex }
         {}
   };

#endif

Затем в тех местах кода, где TSan ругался на потенциальную инверсию порядка захвата mutex-ов, std::lock_guard был заменен на tsan_friendly_lock_guard. И оно сработало: https://godbolt.org/z/fb84zsr4M.

четверг, 16 июля 2026 г.

[prog.c++.multithreading] Теперь уж точно false positive в thread sanitizer-е

Следом за предыдущей, нашел еще одну неприятную ситуацию с thread sanitizer. Но теперь это на 100% ложно позитивное срабатывание.

Посмотреть можно на godbolt: https://godbolt.org/z/zfGfhq89d

Суть в том, что в одной нити захватывается сперва mutex у child-а, а затем, при все еще захваченном mutex-е child-а, захватывается mutex у parent-а.

А потом, когда все ранее захваченные mutex-ы освобождены, уже на другой нити сперва захватывается mutex у parent-а, а следом, не отпуская mutex parent-а, захватывается mutex у child-а.

Thread sanitizer выдает предупреждение о потенциальном дедлоке из-за инверсии порядка захвата мутексов.

Только вот здесь эта инверсия невозможна в принципе, т.к. сперва гарантированно заканчиваются все операции с child-ом, и лишь затем стартует нить, на которой делаются манипуляции с parent-ом.

И вот как удовлетворить thread sanitizer, чтобы он в данном месте не выдавал свою диагностику... Это пока для меня большой вопрос.

Upd. Похоже, это уже известная проблема. С 2022-го года.

Upd2. Найденный обходной маневр: вспомогательный класс tsan_friendly_lock_guard.

среда, 15 июля 2026 г.

[prog.c++.multithreading] Интересно, это false positive от thread sanitizer-а или нет?

Примечание: первоначальный вариант этого поста описывал мое ошибочное предположение о том, что thread sanitizer выдал ложное срабатывание. Однако, ув.тов.Николай Меркин (кому-то он известен как Кодт с RSDN) указал на реальную ошибку. Поэтому текст был переработан.

Thread sanitizer выдал предупреждение на код, который я много лет считал корректным.

Для нетерпеливых вот самодостаточный пример на godbolt: https://godbolt.org/z/3xPadcnva.

Для всех остальных пояснение:

  • на главной нити создается объект actual_repo. В этом объекте живут и std::mutex, и condition_variable (на котором будет осуществляться ожидание);
  • ссылка на actual_repo передается в дочернюю нить. Через какое-то время дочерняя нить вызывает для actual_repo метод stop;
  • главная же нить засыпает на вызове wait_for_stop у объекта actual_repo. Этот метод вернет управление только после того, как дочерняя нить вызовет stop;
  • когда дочерняя нить вызывает stop, то главная нить просыпается, выходит из wait_for_stop, после чего разрушается объект actual_repo;
  • после чего дожидаемся завершения дочерней нити и прекращаем работу.

Фокус здесь в том, что внутри stop условная переменная взводится (вызов notify_one()) без захвата мутекса.

А это ведет к тому, что главная нить может проснуться и уничтожить объект actual_repo еще до того, как дочерняя нить завершит вызов stop.

Т.е. деструктор для repo_basic::m_stop_initiated_cv может отработать еще до того, как на дочерней нити завершится вызов m_stop_initiated_cv.notify_one().

И как раз thread sanitizer и ругается на то, что в главной нити происходит модификация содержимого repo_basic::m_stop_initiated_cv тогда как на дочерней нити мы это содержимое только только прочитали.

Проблема же оказалась в том, что метод stop, вызванный на дочерней нити, не является атомарным. В нем сперва вызывается try_initiate_stop из базового класса. В этом самом try_initiate_stop захватывается mutex, меняется значение m_status, после чего mutex освобождается. Управление возвращается в метод stop и только после этого взводится m_stop_initiated_cv.

Именно эта неатомарность и является корнем зла.

Главная нить в методе wait_for_stop может захватить mutex и проверить m_status как раз в момент, когда на дочерней нити завершился try_initiate_stop, но еще не было обращения к m_stop_initiated_cv. И если такое произойдет, то главная нить уничтожит объект actual_repo еще до того, как на дочерней нити произойдет вызов m_stop_initiated_cv.notify_one().

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

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

Так что в данном случае thread sanitizer выявил реальную проблему.

пятница, 3 апреля 2026 г.

[prog.c++] Эх, давно я не брал в руки SObjectizer...

Недавно в проекте у клиента наткнулись на странное поведение mimalloc-а в одном из многопоточных сценариев использования. Дабы исключить фактор собственных ошибок понадобилось сделать минимальный пример, на котором это странное поведение воспроизводится. Ну и чтобы пример был минималистичным, то пришлось воспользоваться только тем, что есть в стандартной библиотеке C++ "из коробки".

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

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

Передачу memory_pool-а в дочернюю нить сделал через переменные, защищенные mutex-ом. А чтобы эффективно ждать появление значений в этих переменных -- std::condition_variable. Чтобы получить уведомление от дочерней нити о том, что memory_pool перестал использоваться, задействуется std::promise и std::future. Как-то многовато для того, чтобы прокинуть одну команду из родительской нити в дочернюю, а затем один сигнал обратно 🙁

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

И вот после того, как все это было сделано, стала терзать мысль о том, что на SObjectizer-е с mchain-ами должно же было бы получиться проще. Терзала она меня, терзала, и в конце-концов заставила потратить немного времени, чтобы сделать вариант на SO-5.

Ну и что хочу сказать? 😉

На SO-5 и компактнее, и проще, и понятнее. На мой сугубо субъективный взгляд.

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

Получается тривиальное взаимодействие: в родительской нити сперва send, затем receive, а в дочерней нити сперва receive из которого уже делается send в обратном направлении.


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

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

  • автоматическое завершение дочерней нити в SObjectizer-варианте как раз уже обеспечивается за счет использования auto_joiner-ов и auto_closer-ов;
  • контроль тайм-аутов ожидания в случае с so_5::receive добавляется элементарно. В случае с примитивами из C++ной библиотеки, в принципе, тоже не сложно, но телодвижений, имхо, все-таки чуть-чуть побольше потребуется.

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

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

[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:

понедельник, 11 сентября 2023 г.

[prog.c++] В склерозник: статья "What every systems programmer should know about concurrency"

Хорошая статья для тех, кто хотел бы в общих чертах понять что такое атомарные операции и с чем их едят: "What every systems programmer should know about concurrency".

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

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

пятница, 28 июля 2023 г.

[prog] Будьте бдительны, если вам потребуется работать с CivetWeb из нескольких тредов

Есть такая чисто Си-шная разработка, CivetWeb, которая позволяет встроить HTTP/WebSocket сервер в ваше приложение. Насколько я понял, там используется модель thread-per-connection или что-то вроде того. Т.е. когда CivetWeb принимает подключение, он создает рабочую нить, на которой CivetWeb и будет работать с этим подключением.

Очень простая модель. Но в случае, если вам доведется обслуживать посредством CivetWeb WebSocket-подключения, нужно проявлять осторожность.

Дело в том, что CivetWeb на контексте своей нити (давайте называть ее IO-thread) будет дергать ваши callback-и: connect_handler (хотя для WebSocket это не так актуально), ready_handler, data_handler и close_handler.

ready_handler будет вызван когда CivetWeb примет WebSocket подключение с той стороны (т.е. после завершения процедуры upgrade protocol). По сути, с этого момента и начинается ваша работа именно с WebSocket-подключением. В ready_handler передается указатель на mg_connection, с которым вы будете иметь дело.

data_handler вызывается, когда CivetWeb вычитывает входящие данные из соединения и отдает их вам на обработку (как раз посредством вызова data_handler callback на контексте IO-thread). И если у вас протокол поверх WebSocket-а вида запрос-ответ, то вы можете сразу здесь, внутри data_handler, выполнить mg_websocket_write. В этом случае у вас все хорошо.

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

Вот здесь нужно проявить осторожность. Т.к. делать вызов mg_websocket_write вы будете на контексте какой-то своей рабочей нити, а не на контексте IO-thread от CivetWeb. А осторожность нужна из-за наличия close_handler callback-а.

Этот close_handler вызывается CivetWeb-ом на контексте IO-thread когда CivetWeb обнаруживает, что соединение разорвано (по инициативе удаленной стороны или из-за ошибки ввода-вывода). И, внимание, после завершения close_handler указателем на mg_connection пользоваться нельзя, он уже протух!

Так вот, засада в том, что у вас есть собственная пишущая нить, на которой вы можете вызывать mg_websocket_write. Но тут вашу пишущую нить приостанавливают, управление получает IO-thread, на которой вызывается close_handler, после которого mg_connection становится недействительным. И после того, как mg_connection стал недействительным, просыпается ваша пишущая нить, на которой и происходит вызов mg_websocket_write. С невалидным указателем на mg_connection, ага.

Чтобы не попадать в такую ситуацию потребуется какой-то механизм защиты для имеющегося у вас на руках mg_connection. Например, вот такой (это псевдокод, без претензии на компилябильность):

пятница, 21 июля 2023 г.

[prog.c++] Тяжко это, разбираться с голой многопоточностью без SObjectizer-а

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

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

В общем, есть объекты A, B и C. У объекта B есть мьютекс, который объект B использует совместно с объектами A и C. При этом объект C является подчиненным для B, т.е. B инициирует создание C и должен среагировать на исчезновение C.

У объектов B и C есть ссылка на объект A. У объекта A есть ссылки на B и C. Объекты A и C дергают методы друг друга. Объект A создает и уничтожает объект B.

И все эти объекты живут на разных нитях.

Вроде бы не должно быть сложно: всего три объекта, всего три нити, всего один мьютекс.

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

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

Хотя, конечно, всегда есть шанс, что это я просто уже совсем старенький, мозги уже не те 🙁

воскресенье, 3 июля 2022 г.

[prog.c++] Сперва набросал черновик класса с фоновой рабочей нитью, а потом понял, что не все так просто

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

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

Итак, было предложено что-то вроде вот этого:

четверг, 25 марта 2021 г.

[prog.c++] Мне кажется, что с SObjectizer-ом гораздо лучше видно где, когда и какие треды создаются внутри приложения

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

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

В SObjectizer ты имеешь дело с агентами. Агенты сами никаких нитей (тредов) не запускают. Такими вещами занимаются диспетчеры.

А места создания новых диспетчеров и привязки агентов к диспетчерам в коде хорошо видны. Так что читая код практически сразу видишь, когда новая рабочая нить запускается.

Тогда как изучая код, использующий голую многопоточность, приходится обращать внимание на то, где появляются экземпляры std::thread (или какие-нибудь самодельные обертки над POSIX Threads). Зачастую такие экземпляры являются членами какого-то класса. Типа service_manager или session_listener. И стартуют/останавливаются рабочие нити в каких-то методах этих самых service_manager/session_listener/им-подобных-классов. Что не так-то и просто обнаружить. А обнаружив потом еще и держать в памяти: "О, вот этот объект запустил рабочую нить, а затем этот объект переходит в подчинение вот этому, а нить останавливается тогда, когда..."

В общем, привыкаешь к хорошему (это я про SObjectizer), расслабляешься, теряешь нюх на неприятности... :(

суббота, 18 мая 2019 г.

[prog.c++] Еще один любопытный баг на стыке многопоточности и ООП

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

Итак, обнаружилось, что один из тестов время от времени падает с диагностикой "pure virtual method called". Разбирательство показало, что проблема проявляется в коде, который похож вот на этот (лишние детали убраны, дабы не можно было рассказывать только о сути проблемы):

class data_owner_t {
public:
   virtual void update() = 0;
   ...
};

class data_repository_t {
   std::mutex lock_;
   some_container_t<data_owner_t *> owners_;
   ...
public:
   void add(data_owner_t & owner) {
      std::lock_guard lock{lock_};
      owners_.insert(&owner);
   }

   void remove(data_owner_t & owner) {
      std::lock_guard lock{lock_};
      owners_.erase(&owner);
   }

   void update_all() {
      std::lock_guard lock{lock_};
      for(auto * p : owners_)
         p->update();
   }
   ...
};

Виртуальный метод здесь всего один -- это data_owner_t::update. Вызывается он только внутри data_repository_t::update_all, в цикле, перебирающем всех зарегистрированных owner-ов. Значит в какой-то момент времени внутри data_repository_t оказывается невалидный указатель на owner-а. Но как и почему?

четверг, 21 марта 2019 г.

[prog.bugs] Интересная ошибка, связанная с многопоточностью

В минувший вторник убил целый рабочий день на разбирательство с любопытным багом. В многопоточном коде, в котором пришлось иметь дело с голыми std::mutex-ами и std::thread. Кому интересно, милости прошу под кат. Ошибка, в общем-то, имеет C++ную специфику, но, полагаю, во что-то подобное можно втоптаться и в любом другом языке с ручным управлением ресурсами.

Итак, суть в том, что в один прекрасный момент тест, который до этого долгое время работал исправно, начал стабильно падать. Причем падал по разному под VC++ и MinGW, что добавило ярких красок в процесс поиска причины проблемы. В итоге длительного выкуривания бамбука и множества экспериментов причина была найдена. Ниже я попытаюсь в максимально упрощенном виде рассказать что происходило и почему это происходило.

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

[prog.c++] Небольшое послесловие про "Обедающих философов" и exception-safety

В статье про реализацию задачи про обедающих философов посредством Actors и CSP я отдельно затронул тему обеспечения exception-safety при использовании модели CSP. И, пожалуй, можно на этом моменте остановится подробнее еще раз.

Давайте представим себе, что мы захотели сделать функцию run_simulation() безопасной по отношению к исключениям.

Первое, что нам придется сделать -- это выполнить рекомендации из статьи по корректному завершению нитей для вилок. Т.е. сперва мы будем закрывать каналы, созданные для вилок, только потом будем вызывать join(). OK. С этим все понятно.

Далее нам нужно будет при возникновении каких-либо проблем завершить нити для философов.

И тут, если мы просто вызовем join(), мы опять наступим на те же грабли, не лежащие несколько по-другому. Дело в том, что внутри philosopher_process есть цикл, в котором выполняются вызовы receive(). Выход из receive() может произойти либо при получении ответа от вилки, либо при закрытии канала.

Но, если нити для вилок уже завершили свою работу, то вилки прислать ответ философу уже не смогут. И канал никто не закроет, т.к. каналом владеет сам philosopher_process, а run_simulation() к каналу доступа не получит.

Значит, каналы для философов мы так же должны создавать в run_simulation(), хранить их в контейнере, а потом принудительно закрывать прежде чем вызывать join() для нитей философов.

OK. Это уже шаг в верном направлении.

Допустим, что мы это сделали. Станет ли наше решение корректным?

К сожалению, нет. Т.к. внутри philosopher_process цикл с вызовами receive(). Из receive-то мы выйдем принудительно закрыв канал. А вот из цикла?

А из цикла мы не выйдем, т.к. для этого нужно увеличивать meals_eaten, а это происходит только при получении taken_t от правой вилки. Но ведь taken_t мы не получим, т.к. и вилки уже перестали работать, и наш канал уже закрыли.

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

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

вторник, 29 января 2019 г.

[prog.c++] "Modern" Dining Philosophers in C++ with Actors and CSP (part 2)

In the previous post we discussed several implementations of "dining philosophers problems" based on Actor Model. In this post I will try to tell about some implementations based on ideas from CSP. Forks, philosophers and waiter will be represented as std::thread and will communicate each other via CSP-channels only.

Source code can be found in the same repository.

CSP-based Implementations

Like in the previous post we will start from implementation of Dijkstra's solution, then we will go to simple solution with putting the first fork if an attempt to take the second fork fails (without arbiter), and then we will go to solution with waiter/arbiter. It means that we will see the same solutions like in previous post, but reimplemented without Actors. Moreover the same set of messages (e.g. take_t, taken_t, busy_t, put_t) from Actor-based implementations will be reused "as is" in CSP-based implementations.

понедельник, 28 января 2019 г.

[prog.c++] "Modern" Dining Philosophers in C++ with Actors and CSP (part 1)

Some time ago a reference to an interesting article "Modern dining philosophers" was published on resources like Reddit and HackerNews. The article discusses several implementations of well-known "dining philosophers problem". All those solutions were built using task-based approach. I think the article is worth reading. Therefore, if you have not read it yet, I recommend read the article. Especially if your impressions of C++ are based on C++03 or more earlier versions of C++.

However there was something that hampered me during the reading and studying proposed solutions.

I think it was usage of task-based parallelism. There are too many tasks created and scheduled thru various executors/serializers and it's hard to understand how those tasks are related one to another, in which order they are executed and on which context.

Anyway the task-based parallelism is not the single approach to be used to solve concurrent programming problems. There are different approaches and I wanted to investigate how a solution of "dining philosophers problem" can looks like if Actors and CSP models will be used.

To do that I implemented some solutions of "dining philosopher problem" with Actors and CSP models. Source code can be found in this repository. In this post you will find description of Actors-based implementation. In the next part I will tell about CSP-based implementations.

понедельник, 21 января 2019 г.

[prog] И еще раз об обозримости task-based подхода

В докладе "Actors vs CSP vs Tasks..." на C++ CoreHard Autumn 2018 (видео тут, текстовая версия тут) я сказал, что когда код пишется с использованием task-based подхода, то обозримость у этого кода получается так себе. А несколько дней назад на Reddit-е всплыла ссылка на статью под названием "Modern dining philosophers". В которой как раз демонстрируется несколько подходов к решению известной задачи "обедающие философы". Но все эти подходы используют task-и.

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

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

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

PS. Я думаю написать статью с разбором решения этой же задачи на базе SObjectizer-овских агентов. Кому интересно подобное "сравнение" (хотя это не будет чистой воды сравнением), не пожалейте времени поставить плюсадынку или лайк. Чем больше оных наберется, тем быстрее появится моя статья.

четверг, 27 декабря 2018 г.

[prog.flame] Эх, если бы с многопоточностью все было так просто...

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

Ни один мейнстрим язык не защитит вас от проблем с многопоточностью на уровне типов.

среда, 7 ноября 2018 г.

[prog.c++] SObjectizer-5.5.23 и so_5_extra-1.2.0

Мы все-таки добрались до релиза SObjectizer-5.5.23 и so_5_extra-1.2.0. Ничего нового по сравнению с тем, что описывалось ранее, в SObjectizer/so_5_extra не попало. Разве что специально удостоверились в том, что SObjectizer может собираться под Android посредством свежих Android NDK (проверялось на r18b).

Официальный анонс можно найти на странице проекта.

Свежую версию SObjectizer-а можно взять либо из секции Files, либо с GitHub-зеркала.

Свежую версию so_5_extra можно взять из секции Files (внутри архивов с именами so_5_extra-1.2.0-full уже находятся все внешние зависимости, включая SO-5, Asio и т.д.).

Пользуясь случаем хочу сказать большое спасибо всем, кто не только помог нам с этим релизом. И вообще всем, кто проявлял интерес к SObjectizer-у на протяжении всех этих лет. Ваше внимание и ваша помощь очень и очень сильно нам помогала. Большое спасибо еще раз.

Уже многократно говорил, но повторю еще раз: SO-5.5 развивается уже более четырех лет. Это большой срок, за это время SObjectizer обзавелся многими вещами, о некоторых из которых мы даже и подумать не могли в свое время. С некоторой ретроспективой интересующиеся могут ознакомиться в свежей статье на Хабре: "Четыре года развития SObjectizer-5.5. Как SObjectizer изменился за это время?"

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

  • подружить so_5_extra с CMake. Хотелось сделать это в рамках 1.2.0, но CMake в очередной раз порадовал своей простотой и понятностью. Пришлось отложить;
  • подружить so_5_extra с Boost.Asio. Сейчас so_5_extra работает только со standalone версией Asio, надо бы поддержать еще и Boost.Asio, как мы это сделали в RESTinio в свое время;
  • добавить в so_5_extra возможности для тестирования агентов. Что-то вроде инструментария для упрощения написания unit-тестов для агентов (с использованием агентов).

По поводу последнего пункта пока много непонятностей. Вероятно, для поддержки тестирования агентов потребуется сделать еще и SO-5.5.24. Будем посмотреть. Но вообще мы уже смотрим в сторону SObjectizer-5.6, где мы выбросим накопившийся в SO-5.5 старый хлам и перейдем на C++14.

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

Ну а если кто-то найдет возможным поделиться в Интернетах своим опытом работы с SObjectizer-ом, то это будет просто неоценимое подспорье для нас. Нам очень не хватает публично доступных success stories. А предоставить их можете только вы. Так что если кто-то может или хочет сказать в наш адрес пару добрых слов, то самое время сделать это ;) Подробности можно обсудить по почте (eao197 на gmail точка com).

В общем, еще раз спасибо. Пробуйте SObjectizer, делитесь своими впечатлениями, высказывайте нам свои замечания. Вместе мы сделаем SObjectizer лучше. Достаточно просто посмотреть на то, что уже было сделано.