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

четверг, 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 выявил реальную проблему.

вторник, 26 мая 2026 г.

[prog.c++.bugs] Похоже наткнулся на баг в GCC 12/13 под Linux-ом. Или нет.

Дело было так: есть некий объемный и сложный шаблон класса-контейнера. Для тестирования было создано приложение с юнит-тестами на базе Google.Test. В состав этого приложения входит порядка 30 (тридцати) .cpp-файлов. В некоторых из них происходит следующее:

namespace
{

template<typename T>
struct test_traits : public my_container::default_traits<T> {
  static constexpr std::size_t key_size = 3;
};

/* namespace anonymous */

TEST(my_container, some_test)
{
  my_container::my_map<int, test_traits> map;
  ... // какие-то действия с map.
}

Т.е. суть в том, что в десятке .cpp-файлов есть анонимные пространства имен, в каждом из которых определяется шаблон класса с именем test_traits. Затем этот шаблон используется для инстанцирования класса-контейнера.

Все это работало до тех пор, пока не был добавлен еще один .cpp-файл, в котором было практически тоже самое:

namespace
{

template<typename T>
struct test_traits : public my_container::default_traits<T> {
  static constexpr std::size_t key_size = 3;
  static constexpr my_container::mode use_mode =
      my_container::mode::versioned;
};

/* namespace anonymous */

TEST(my_container, some_test_versioned)
{
  my_container::my_map<int, test_traits> map;
  ... // какие-то действия с map.
}

И вот тут-то в some_test_versioned с map стали происходит странные вещи: возникали segmentation faults там, где их быть не должно было. Попытки отладить код приводили к тому, что отладчик показывал, что отрабатывают не те ветки if-ов. А отладочные печати содержали совсем не те значения, которые должны были бы быть.

Было полное ощущение, что GCC сошел с ума.

Проект, в рамках которого все это делается, собирается VC++ под Windows и GCC под Linux-ом. Под Linux-ами используются GCC 12 и 13. Конкретно я работаю с GCC 13, но проверил и под GCC 12. Сам проект уже не очень маленький, плюс подтягивает кучу зависимостей разного калибра (включая Folly и Abseil). Все это к тому, что мероприятие по перекомпиляции проекта под какой-то свежий GCC или clang -- это попытка с негарантированным результатом. Может повезти, а может и нет.

Под Windows проверил, там ничего подобного нет, все работает как и положено. А вот под Linux-овым GCC -- проблемы.

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

После этого все описанные выше магические проблемы разом исчезли.

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

Но на 100% не уверен. Может быть здесь дело еще и в том, что у my_container::map есть шаблонный параметр шаблона, т.е.:

namespace my_container
{

template<typename T, template<typenameclass Traits>
class map { ... };

/* namespace my_container */

Поэтому его параметризация в тесте идет не конкретными типами, а шаблоном:

TEST(my_container, some_test_versioned)
{
  my_container::my_map<
    int// Это конкретный тип.
    test_traits // А это шаблон, который развернется в конкретный
                // тип уже внутри map.
  > map;
  ... // какие-то действия с map.
}

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

четверг, 21 мая 2026 г.

[prog.c++] Обнаружился баг в timertt возрастом более 10 лет

Пользователи обнаружили в SObjectizer проблему, которая была вызвана неправильной работой механизма timer_heap в библиотеке timertt.

Эта библиотека написана мной осенью 2014-го года для того, чтобы можно было окончательно отвязать SObjectizer от ACE. И как раз тогда, чуть ли не в самой первой версии, допущена ошибка в операции удаления таймерной заявки в механизме timer_heap. Этот timer_heap реализован в виде binary heap на базе вектора. И как раз удаление из вектора и содержало проблему.

То, что я допустил достаточно дурацкую ошибку совсем не удивительно. Я вообще умудряюсь делать на удивление много ошибок при реализации простых структур данных (скажем, если приходится вручную программировать интрузивный двусвязный список, то я там обязательно в паре мест накосячу). Дополнительным отягчающим фактором стало то, что специфическое для timer_heap тестирование было проведено "по верхам". Думаю, что если бы в 2014-ом не поленился составить тест на базе примитивного fuzzing-а, то эта проблема вскрылась бы уже тогда. Но невнимательность + разгильдяйство сделали свое темное дело.

Более удивительно то, что этот баг проявился в полный рост только сейчас, в 2026-ом. Вот это внушаить 🤔

Какие выводы можно сделать?

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

PS. Что меня еще очень сильно удивило, так это то, что люди смогли найти проблемное место в не самом тривиальном (даже для меня) коде. И, к тому же, предложили патч на базе которого я в итоге и сделал исправление. Значит пишу не такой уж и страшный код, если в нем можно разобраться.

PPS. Видимо, нужно найти время и вытащить timertt из старого svn-репозитория на SourceForge чтобы он продолжил жить на GitHub-е. Плюс выбросить оттуда MxxRu и перевести все на CMake (собственно, необходимость бодаться с CMake и является основным стоп-фактором). Нужно как-то себя заставить сделать это. Жаль только, что история коммитов при переносе в git потеряется 🙁

PPPS. Обновление для SObjectizer-а уже опубликовано в виде версии 5.8.5.1.

среда, 29 октября 2025 г.

[prog.c++.bug] Забавный баг подсадил давеча в код

Любопытный случай, достойный того, чтобы быть сохраненным на память.

Был код по типу вот такого:

do {
  ... // Что-то делаем.
} while(!cnt.empty());

Т.е. выполнение каких-то действий до тех пор, пока контейнер не пуст.

После внесения в код новой функциональности данный фрагмент принял вид:

while(a < params.max_value && !cnt.empty()) {
  ... // Что-то делаем.
} while(!cnt.empty());

Т.е. do я убрал и поставил while, но тот while, который остался от do, не удалил 🙁

И, что самое забавное, этот код у меня работал без проблем 🧐
Я даже не знал, что проблема существует, пока коллеги не подсказали.

Очень редко компилируюсь в режиме Debug. В основном в Release, иногда в RelWithDbgInfo. Но не в Debug.

А как раз в Debug ошибка и проявилась. Оставшийся while начал работать как бесконечный цикл.

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

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

пятница, 1 августа 2025 г.

[prog.c++.bugs] Пример типичной C++ной ошибки

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

Вся суть вот в этом фрагменте:

class lock_getter
{
   const std::chrono::steady_clock::duration & m_wait_time_limit;
   ...
   std::condition_variable m_wakeup_cv;
   ...
   bool m_access_granted;

public:
   lock_getter(
      ...,
      std::chono::steady_clock::duration wait_time_limit,
      ...)
      : ...
      , m_wait_time_limit{ wait_time_limit }
      , ...
   {}
   ...
private:
   void try_acquire_or_wait()
   {
      ...
      m_access_granted = false;
      m_wakeup_cv.wait_for(m_lock, m_wait_time_limit,
            [this]() { return m_access_granted; });
      if(!m_access_granted)
         throw std::runtime_error{ "lock can't be acquired" };
      ...
   }
};

Нить A пыталась захватить некий ресурс, который ей по запросу должна была отдать нить B. В ожиданнии подтверждения нить A засыпала, как раз в методе try_acquire_or_wait. Нить B точно разрешала нити A захват ресурса и вызывала для m_wakeup_cv метод notify_one (т.е. точно будила нить A). Но проснувшись нить A почему-то считала, что ресурс ей не дали и порождала исключение. Хотя ресурс ей дали. Но нить A все равно считала, что нет, и бросала исключения.

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

Кому лень смотреть, милости прошу под кат.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

четверг, 21 декабря 2023 г.

[prog.bugs] Иногда цена ошибки может быть известна достаточно точно

Некоторое время назад один разработчик вызвал некую функцию и забыл проконтролировать ее код возврата. В большинстве случаев эта функция завершалась нормально и все работало как полагается.

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

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

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

ЗЫ. Все совпадения с реальными людьми и событиями случайны и непреднамеренны :)


Мораль сей басни: если вызывается какая-то функция, которая возвращает код ошибки, то код ошибки обязательно должен быть проверен. Обязательно. Должен. Быть. Проверен.

Далее, в случае возникновения ошибки, по ситуации:

  • если ошибка ожидаемая, то выполняется логика ее обработки. Например, попробовали открыть файл, не получилось, что-то сделали по этому поводу. Скажем, выдали сообщение пользователю и попросили ввести новое имя файла;
  • если ошибка не сильно ожидаемая и не предполагающая путей исправления здесь и сейчас, то либо возвращаем свой код ошибки наверх (если исключения под запретом), либо выбрасываем исключения. Например, вызвали malloc, а он взял и вернул NULL. Маловероятно, но потенциально может произойти. Проверили результат malloc-а и вернули код ошибки наверх;
  • если ошибка вообще из разряда невероятных, то либо действуем как в предыдущем пункте, либо же вообще тупо зовем abort. Например, вызываем getwd, а получаем NULL, хотя казалось бы как такое возможно?

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

  • во-первых, сразу же узнаете о проблеме и
  • во-вторых, не сможете тихо "замести ее под коврик".

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


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

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

[prog.c++] Давненько не сталкивался с неспособностью компилятора переварить наш C++ный код...

Готовлю к релизу небольшое обновление для RESTinio. Там суть в том, что в fmtlib есть возможность контроля за форматной строкой в compile-time. Для этого, когда мы находимся в рамках стандартов C++11/14/17, требуется помещать форматную строку внутрь макроса FMT_STRING:

fmt::print(FMT_STRING("The answer is {}\n"), 42);

Фокус в том, что макрос FMT_STRING должен применяться когда задан символ препроцессора FMT_ENFORCE_COMPILE_STRING. Если этот символ не задан, то форматная строка должна быть обычным строковым литералом.

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

Вроде как все сделал еще три недели назад, но приступить к подготовке релиза выдалась возможность только сейчас. Заодно оказалось, что fmtlib обновился до 9.1.0, поэтому я решил проверить RESTinio еще раз, уже с более свежей fmtlib.

И тут-то и оказалось, что в режиме C++20 и FMT_ENFORCE_COMPILE_STRING пара штатных тестов и один пример не компилируются clang-14.

Компилятор clang-14 как-то матерно ругался вот на такие строчки в одном из тестов (раз и два). Мол, какой-то из dependent type где-то в нутрях fmtlib не определен. А где и какой непонятно.

Пришлось несколько часов курить бамбук, пробовая и так, и сяк. Особенно удивляясь тому, что gcc-11 проглатывает этот же код нормально. Да и сам clang-14 похожий код в других местах вполне себе компилирует.

Лучик света забрежжил, когда я закомментировал вызов fmt::format на самом глубоком уровне вложенности (вот здесь).

Оказалось, что оставшийся вызов fmt::format после этого успешно скомпилировался, хотя до этого clang на него ругался.

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

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

Вуаля! Все скомпилировалось.

Морали не будет. Но будет озвучен вопрос, который меня серьезно озаботил: ну ладно, я-то давно люблюсь с C++ и C++ными компиляторами, падения с internal compiler error встречал неоднократно (к счастью, в последние годы все реже и реже)... А вот что было бы, если бы на моем месте был человек менее опытный? Который бы реально полез бы в потроха fmtlib чтобы разобраться что там за dependent type не определен... Вот сколько бы он времени на это убил бы?

пятница, 15 июля 2022 г.

[prog.c++.bugs] Вот так всегда: как только видишь рукопашный new/delete, так жди какой-нибудь бяки :(

Что называется краешьком глаза решил глянуть...

Если я еще не забыл C++, то для new T[] должен применяться delete[], а не просто delete.

Цинк, если что.

Где-то там же увидел и еще один фрагмент, от которого глаз дернулся:

суббота, 7 мая 2022 г.

[prog.c++] Еще один связанный с многопоточностью баг

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

Итак, есть объект acquisition_manager, который владеет несколькими acquisition_thread. Передача информации между acquisition_manager и acquisition_thread происходит через объекты gate: для каждого acquision_thread создается своей gate, ссылка на который отдается в конструктор acquision_thread.

Периодически acquision_manager наполняет объекты gate параметрами, после чего дает сигнал acquision_thread выполнить нужную работу и поместить в gate результаты, а когда acquisition_thread завершает свою часть работы, то acquisition_manager забирает результаты из все того же объекта gate.

Для взаимодействия между acquisition_manager и acquisition_thread у класса acquisition_thread есть методы:

суббота, 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, что добавило ярких красок в процесс поиска причины проблемы. В итоге длительного выкуривания бамбука и множества экспериментов причина была найдена. Ниже я попытаюсь в максимально упрощенном виде рассказать что происходило и почему это происходило.

среда, 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-ом в виртуалке эта вероятность материализовалась. Причем достаточно стабильно. Так что тут мне изрядно повезло.

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

суббота, 24 сентября 2016 г.

[prog.c++] Переполенные mchains и доставка отложенных/периодических сообщений

Пока готовил очередную статью для Хабра, выяснил, что в SObjectizer при добавлении message chains (это нечто вроде CSP-шных каналов) был допущен серьезный просчет. Дело вот в чем: mchain-ы могут использоваться для отсылки отложенных и периодических сообщений. Т.е. можно вызывать send_delayed или send_periodic, а в качестве адресата указать mchain. И сообщение "упадет" в этот mchain спустя указанное время.

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

  • можно подождать какое-то время на send-е. Если за это время место в mchain-е освободилось, то просто добавить сообщение в mchain и все. А вот если мы подождали, но места не нашлось, тогда идем к следующему пункту. Впрочем, можно сконфигурировать mchain так, чтобы ожидания вообще не было. Тогда мы сразу же идем к следующему пункту;
  • т.к. места в mchain-е нет, то SObjectizer смотрит на параметр overflow_reaction для mchain и:
    • в случае drop_newest просто игнорирует новое сообщение, которые мы пытаемся добавить в mchain;
    • в случае remove_oldest выбрасывает самое старое сообщение из mchain-а, а новое -- добавляет в mchain;
    • в случае throw_exception выбрасывает самое новое сообщение и генерирует исключение;
    • в случае abort_app просто вызывает std::abort.

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

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

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

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

Тем не менее, все версии SO-5 с поддержкой mchain-ов, включая последнюю стабильную 5.5.17.1, не учитывают этих ограничений для контекста таймерной нити. И, если пользователь вызывает send_delayed для ограниченного по размеру mchain-а с ожиданием на переполнении и с реакций throw_exception, то когда время доставки сообщения наступит, а mchain будет полон, то сперва нить таймера заснет на этом mchain-е, затем будет брошено исключение, от которого все приложение упадет из-за вызова std::abort.

Такой вот недосмотр.

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

  • если нить таймера обнаруживает, что отложенное/периодическое сообщение идет в переполненный mchain, то ожидание на этом mchain-е не производится, даже если такое ожидание предписано в параметрах mchain-а. Нить таймера просто сразу начнет обрабатывать overflow_reaction. Без каких-либо задержек и ожиданий;
  • вместо throw_exceptio нить таймера выполняет реакцию drop_newest, т.е. простое выбрасывание сообщения, как будто его и не было.

Эти исправления уже реализованы и протестированы. Но в основную ветку я их пока не включил. Хочу послушать другие мнения. Может есть какие-то другие подходы к решению проблемы выполнения overflow_reaction на контексте нити таймера?

четверг, 7 апреля 2016 г.

[prog.mqtt] Интересная багофича MQTT, QoS=0 и pingreq/pingresp

Обнаружил интересную багофичу протокола MQTT. Если некоторый клиент подключается к брокеру и подписывается на некоторый топик с Quality-of-Service=0 (т.е. доставка сообщений не гарантируется), то в случае отсутствия сообщений в этом топике клиент и брокер будут обмениваться pingreq/pingresp-ами. И соединение будет продолжать жить, т.к. оно доказывает свою жизнеспособность этими самыми регулярными ping-ами.

Однако, если в топике появляется сообщение, оно доставляется до клиента посредством publish PDU. Но в ответ клиент ничего не отсылает, т.к. QoS=0 (при этом QoS подтверждения доставки не требуется). И здесь оказывается забавная ситуация: клиент видит у себя активность в канале и начинает отсчет времени для следующего pingreq заново. Т.е. клиент инициирует следующий pingreq через N секунд после получения publish, а не через N секунд после выдачи последнего pingreq. Т.е. если последний pingreq был в момент времени T(0), а publish пришел в момент времени T(1), то следующий pingreq от клиента уйдет в (T(1)+N).

А вот брокер ничего не видит от клиента. Брокер знает только про последний pingreq, полученный в момент времени T(0). И рассчитывает получить следующий в (T(0)+N). Но этого pingreq-а не будет, т.к. клиент отошлет его только в (T(1)+N). Поэтому в (T(0)+N) сервер решит, что клиент умер, а соединение "протухло". Что даст ему право соединение с клиентом разорвать.

Получается, что если некоторый клиент подключается к MQTT-брокеру только для того, чтобы слушать какие-то топики с QoS=0, но ничего не публиковать (например, клиент слушает $SYS-топики), то связь клиента с брокером может рваться на регулярной основе.

Причем, по стандарту, pingreq может отсылать только клиент. Брокер этого не делает. А если бы делал, то этой проблемы бы не было, просто брокер отослал бы pingreq сам, получил бы pingresp и понял бы, что клиент жив-здоров. Но нет, современный MQTT-протокол вот такой.

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

PPS. Не знаю, как на счет реализаций MQTT для других современных языков, но вот C-шные libmosquitto и Paho оставляют ощущение, что они были написаны для каких-то ну очень примитивных сценариев использования.

суббота, 2 апреля 2016 г.

[prog.c++] Похоже, наткнулся на баг в С++ компиляторе из Visual Studio 2015 Update 2

Установил себе сегодня Visual Studio 2015 Update 2, попытался скомпилировать SObjectizer. Столкнулся с тем, что перестал работать один из тестов. Тест выбрасывал исключение из функции, которая вызывается из noexcept-функции. Это должно приводить к аварийному завершению теста. Но не приводит.

Грешу на проблему в обновленном VC++, ибо предыдущий компилятор отрабатывал нормально. Грубо говоря, вот такой код:

templatetypename LAMBDA, typename MESSAGE >
class lambda_as_filter_t : public delivery_filter_t
   {
      LAMBDA m_filter;

   public :
      lambda_as_filter_t( LAMBDA && filter )
         :  m_filter( std::forward< LAMBDA >( filter ) )
         {}

      virtual bool
      check(
         const agent_t & /*receiver*/,
         const message_t & msg ) const noexcept override
         {
            return m_filter(message_payload_type< MESSAGE >::payload_reference( msg ));
         }
   };

Должен приводить к краху приложения, если при вызове m_filter(msg) выскакивает исключение. Но не приводит.

Если же сделать вот такой workaround:

templatetypename LAMBDA, typename MESSAGE >
class lambda_as_filter_t : public delivery_filter_t
   {
      LAMBDA m_filter;

      bool
      do_check( const MESSAGE & m ) const noexcept
      {
         return m_filter( m );
      }

   public :
      lambda_as_filter_t( LAMBDA && filter )
         :  m_filter( std::forward< LAMBDA >( filter ) )
         {}

      virtual bool
      check(
         const agent_t & /*receiver*/,
         const message_t & msg ) const noexcept override
         {
            return do_check(message_payload_type< MESSAGE >::payload_reference( msg ));
         }
   };

То все начинает работать так, как и ожидалось: приложение падает.

Подозреваю, что здесь сказывается то, что базовый класс, delivery_filter_t, из которого наследуется метод check(), экспортируется из DLL. А конкретные экземпляры lambda_as_filter_t генерируются внутри приложение, которое линкует эту DLL. Но пока минимального примера, явно указывающего наличие этой проблемы, сделать не удалось (я пробовал без DLL-ек, но баг не проявился). Может, однако, еще какие-то проблемы сказываются.

В общем, неприятная штука. Как бы не пришлось себе откатывать VS на Update1.

Upd. Засабмитил в Microsoft. Посмотрим, что ответят. И ответят ли.

среда, 28 октября 2015 г.

[prog.c++.bugs] Как я одну серьезную ошибку профукал

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

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

воскресенье, 23 августа 2015 г.

[prog.c++11] В одном из случаев GCC 4.9/5.1 поддерживают шаблоны хуже MSVS2013/2015 :(

На мой дилетантский взгляд GCC всегда поддерживал C++ные шаблоны лучше и строже, чем VC++. Но вот наткнулся на ситуацию, когда GCC 4.9.2/5.1.0 отказывается компилировать код, который успешно проглатывают MSVS2013, MSVS2015, clang 3.4 и 3.6. При этом GCC 5.2.0 уже воспринимает этот код нормально.

Upd. Найден случай, когда все проглатывается.

среда, 22 июля 2015 г.

[prog.bugs] Похоже, накрылся медным тазом мой ArchLinux под VirtualBox :(

Upd. Сделал себе новую инсталляцию ArchLinux, взяв за основу ISO-шку версии 2015.07. Так что свои проблемы с наличием Linux-а для тестов проектов я решил. Тем не менее, спасибо всем за рекомендации! Полагаю, они обязательно пригодятся в будущем.

Upd. Похоже, проблемы были из-за того, что я неправильно перезагрузил Linux после обновления. И что-то в образе диска потерялось. Загрузился с ISO-шного образа, выполнил fsck для /dev/sda1. После чего ArchLinux начал загружаться, но с сообщениями "Failed to start Load Kernel Modules". Как это исправить не знаю. После загрузки сеть не видит в упор.

Давно не запускал, решил обновить, дабы дистрибутив не был слишком древним. Предварительно обновил VirtualBox до 4.3.30. После чего загрузил ArchLinux, зашел под root-ом, выполнил pacman -Syu. Pacman отработал успешно, выкачав и установив кучу обновленных пакетов. Но при попытке перезагрузить Linux вот на этой стадии стал ломаться сам VirtualBox:

Откат на VirtualBox 4.3.28 или обновление до 5.0.0 картинки не меняет. Такое ощущение, что именно обновление в ArchLinux жестко ломает виртуалку.

При этом FreeBSD 10.1 под VirtualBox продолжает работать нормально (накатывать там обновления уже совсем стремно [Upd. pkg upgrade отработало успешно, проблем не возникло] :)).

Вот и фиг знает, что теперь с этим делать. В принципе, потерять этот ArchLinux не страшно, там нет ничего уникального, хотя радости это не принесет. Но тогда возникает другой вопрос: если данную VM спасти уже нельзя, то что имеет смысл взять вместо? Хотелось бы иметь готовый образ Linux-а под VirtualBox, но без X-ов и каких-либо KDE/Gnome/etc. Главное требование -- довольно оперативное появление в дистрибутиве новых версий GCC и Clang, т.к. Linux мне нужен просто в качестве хоста для тестов под GCC/Clang. Ну и чтобы обновление пакетов происходило с минимальным участием пользователя (как в Arch-е, запускаешь pacman, а он сам все делает).