пятница, 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.eiffel] Сохраню в склерозник пример работы с anchored-типами в Eiffel

Есть очень прикольный язык программирования -- Eiffel. А в нем есть очень прикольная фича -- заякоренные типы (anchored types). Давеча пришлось в одном из разговоров про нее вспомнить и набросать небольшой пример, чтобы проверить те или иные предположения.

У Eiffel-я есть он-лайн компилятор (что-то по типу godbolt, wandbox, ideone), но в нем нельзя просто так сохранить сделанное и поделиться публичной ссылкой. Поэтому помещу под кат код написанного примера, просто на память, а то вдруг еще раз потребуется.

В чем суть примера?

Есть базовый класс MESSAGE.

Есть базовый класс ENVELOPE, который хранит в себе MESSAGE. Но класс ENVELOPE написан с использованием MESSAGE в качестве якорного типа. Что позволяет создать наследника SIGNED_ENVELOPE, который хранит уже не просто MESSAGE, а SIGNED_MESSAGE. Но менять унаследованные из ENVELOPE методы не нужно, Eiffel сам разбирается с тем, что в SIGNED_ENVELOPE методы make и change_content получают уже не MESSAGE, а SIGNED_MESSAGE.

При этом Eiffel что-то может проверить в compile-time. Например, если у нас есть ссылка signed_env с типом SIGNED_ENVELOPE, то в вызов signed_env.change_content мы не может отдать просто ссылку на MESSAGE. Будет ошибка компиляции, компилятор ждет от нас SIGNED_MESSAGE (или наследника SIGNED_MESSAGE).

Но если у нас есть env типа ENVELOPE и мы env присвоили signed_env (т.е. теперь env ссылается на экземпляр SIGNED_ENVELOPE), то компилятор пропустит передачу обычного MESSAGE в env.change_content. Но ошибка будет диагностирована в run-time с выбросом исключения. Т.е. "обмануть" Eiffel не получится: то, что Eiffel не смог поймать в compile-time, он поймает в run-time.

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

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

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

Фильмы

Грязные деньги (In The Grey, 2026). Тупо, прямолинейно, приторно красиво, но бодренько. За неимением лучшего можно посмотреть, чтобы скоротать вечер.

Мандалорец и Грогу (The Mandalorian and Grogu, 2026). Отличная сказочка для семейного просмотра с детьми младшего школьного возраста. Разве что немного мрачноватая. В любом случае кино абсолютно детское, взрослым к нему всерьез относиться не стоит.

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

Ограбить Лондон (Fuze, 2025). Ничего не ждал от фильма вообще, думал обычная третьесортная поделка. Оказалось на удивление хорошо. Т.е. это далеко не шедевр, уровнем чуть повыше среднего, но на фоне всего остального шлака, прям глоток свежего воздуха.

Охота за тенью (Shadow's Edge, 2025). Очень бодрая сказочка. Джеки Чану мое почтение.

Мусорщик (Wasteman, 2025). Мне показалось, что это неплохая тюремная драма. Но, во-первых, понятия не имею насколько это достоверно. И, во-вторых, смотрел на скорости 1.25, потому что на нормальной скорости было ну как-то совсем уж нудно. Тем не менее, развязка понравилась.

Сериалы

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

Заложник (первый сезон, 2026). Начало бодрое, затем все скатилось в какую-то сопливую Санта-Барбару. При этом сериальная затянутость ощущается даже не смотря на то, что в сезоне всего шесть серий. Ну и отдельно не понравилось то, что повествование оборвали на полуслове с явным намерением продолжить в следующем сезоне. Снято, однако, качественно. К картинке претензий нет.

Бухта вдов (Widow's Bay, первый сезон, 2026). Скучно, что-то реально интересное и удивляющее происходит очень редко. Сама история толком ничем не закончилась, тут нам явно предлагают ждать следующих сезонов. Купился на хвалебные отзывы в Интернете, по итогу не понравилось, жаль потраченного времени.

Красота (The Beauty, первый сезон, 2026). Первые серии хорошие -- тут и экшен, и драйв, и многообещающая завязка. Затем темп повествования потерялся, драйв улетучился. Финала как такового у первого сезона нет. Создатели обрывают происходящее буквально на полуслове. Возможно, когда выйдет второй сезон, и история достойным образом разовьется, то этот сериал можно будет хоть как-то оценить. Но не в его текущем виде.

Не смог досмотреть

Опасные отношения (Over Your Dead Body, 2026). Это ремейк фильма Поездка (I onde dager, 2021). И, как по мне, так оригинал смотреть интереснее, в нем как-то все более естественно.

Дьявол носит Prada 2 (The Devil Wears Prada 2, 2026). Происходящее на экране невозможно воспринимать серьезно.

Вне категории

Ангелы Ладоги (2026). Такое впечатление, что фильм рассчитан совсем на другое поколение. Возможно, на современных детей 8-10 лет. Как мне его оценивать непонятно.

Литвяк (2026). С одной стороны, хороший военный фильм, без пьяных командиров, трусливых комиссаров и неадекватных СМЕРШев с наганами в руках. С другой стороны, рассказанная история нисколько не зацепила, главные герои показаны так, что не успеваешь к ним прикипеть так, чтобы их гибель заставляла тебя переживать именно потому, что ты успел проникнуться к ним какими-то чувствами. Кроме того, напрягает и раздражает то, насколько много сцен в фильме снято на фоне зеленого экрана. В общем, не знаю, как оценивать. За попытку снять хороший фильм о войне -- отлично, за исполнение -- незачет 🙁

понедельник, 29 июня 2026 г.

[prog.thoughts] Начинаю думать, что бесстековые короутины в C++ следовало делать чуть иначе

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

Изначально я планировать только позволить пользователю описывать event-handler-ы, в которых можно было бы делать co_await. Но по мере продумывания способов реализации столкнулся с тем, что не имею на руках никаких механизмов запуска внешних по отношению к SO-5 короутин. А это нужно, чтобы проверить, что из event-handler-а можно дернуть, скажем, короутину из Asio, и когда она завершится, управление должным образом вернется event-handler-у.

И тогда появилась мысль сделать сперва возможность асинхронной работы с mchain-ами. Сейчас в SO-5 есть синхронные версии receive и select, а что, если предоставить их асинхронные версии? Тогда в event-handler-е можно было бы написать что-то вроде:

so_5::event_handler_task_t
some_agent::evt_some_handler( mhood_t<some_msg> cmd )
{
  auto result = co_await so_5::async_receive( so_5::from( test_chain ), ... );
  ...
}

Тогда отправляя (или не отправляя) сообщения в тестовый канал я бы мог тестировать поведение event-handler-ов.

Но стоило погрузиться в задачу создания асинхронных версий async_receive и async_select-а, как стало возникать подспудное подозрение, что с C++ными бесстековыми короутинами что-то не так. И я не говорю про их мудреность (это тема отдельного разговора). Было ощущение, что несмотря на заумность и гибкость C++ных короутин в них все-таки чего-то важного не хватает.

А выкристаллизовалось понимание чего же именно не хватает в процессе знакомства с библиотекой Capy (которую сейчас пытаются запихнуть в Boost). Как раз авторы данной библиотеки четко выделили проблему, которую не удавалось нащупать мне. И попробовали ее решение прикрутить сбоку синей изолентой.

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

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

В Capy ее предлагают решать за счет введения специальной сущности -- Executor-а и за счет изменения интерфейса сущности Awaiter-а: метод await_suspend для Awaiter-а вместо одного аргумента получает два:

std::coroutine_handle<> await_suspend(std::coroutine_handle<> h, io_env const* env);

И во втором параметре передаются вещи, которые могут потребоваться короутине (и ее дочерним короутинам): executor, stop_token, allocator.

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

Это как раз то, чего мне не хватало для реализации асинхронных версий receive/select. Я уже сам пришел к мысли о том, что в асинхронный receive нужно передавать какой-то coro_scheduler, который будет отвечать за то, чтобы возобновить приостановленный receive/select именно там, где это разрешено. Например, если receive вызывается из event-handler-а, то это могло бы выглядеть так:

auto result = co_await so_5::receive(
  so_5::from( test_ch ).resume_by( this->so_coro_scheduler_for_this_agent() )...,
  ... );

А если mchain используется вне агентов, то может быть что-то вроде:

so_5::cpp_coro::this_thread_scheduler_t coro_scheduler;
coro_scheduler.sync_wait(
  [&coro_scheduler, &ch]() -> so_5::cpp_coro::async_receive_task_t {
    co_await so_5::receive(
      so_5::from( ch ).resume_by( coro_scheduler )..., ...);
  } );

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

Но, повторюсь, и подход Capy, и мои попытки придумать некий условный coro_scheduler для SObjectizer-а -- это попытки прикрутить решение сбоку посредством синей изоленты.

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

Например, чтобы обращение к co_await можно было параметризовать. Скажем, передавать executor/scheduler непосредственно в co_await:

co_await(executor) some_task();

И чтобы этот executor передавался бы параметром в await_resume. Может быть в виде ссылки на некоторый специальный объект environment, как это делается в Capy.

Есть у меня подозрение, что с такой явной передачей executor-ов в co_await, код с короутинами и писать, и читать было бы гораздо проще.

PS. Раз уж заговорил про попытку добавить поддержку короутин в SO-5, то слегка обозначу текущий статус. Пока до чего-то работающего еще далеко. Пытаюсь двигаться от простого к более сложному. Сперва хочу попробовать сделать асинхронную версию receive/select. Чтобы с ее помощью перейти к поддержке event-handler-ов в виде короутин. Затем, возможно, попробую погрузиться еще глубже, чтобы разрешить короутины в роли обработчиков входящих сообщений в receive/select (если это вообще возможно). Работы много, ресурсов мало, движется все очень медленно. Поэтому я сам в течении ближайших пару месяцев никаких значимых результатов не жду.

понедельник, 15 июня 2026 г.

[prog.c++.imho] Не согласен с постулатами пропозала P3097 (контракты для виртуальных методов)

Комитет по стандартизации C++ продолжает творить дичь. Сперва в C++26 были включены кастрированные контракты (нет ключевого слова old в постусловиях, нет контрактов для виртуальных методов, нет инвариантов для экземпляров классов и циклов). Для людей, знакомых с Eiffel, контракты из C++26 выглядят как "мы не осилили тему полностью, поэтому впихнули в стандарт какой-то эрзац с надеждой, что со временем допилим". Не хочу обсуждать зачем нужен эрзац вместо нормального продукта. Просто перейду к следующей дичи.

Далее в C++29 включили предложение P3097, которое описывает контракты для виртуальных методов классов. И авторы этого предложения, как по мне, покусились на святое: на сформулированное много-много лет назад для Design By Contract в Eiffel-е требование о том, что производный класс может только ослабить предусловния и ужесточить постусловия, но не наоборот.

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

На протяжении нескольких страниц пропозала эти люди пытаются приводить "аргументацию" своей точки зрения. Меня эта аргументация не убеждает от слова совсем. Скорее наводит на мысль о том, что люди толком не понимают тему, о которой пытаются рассуждать и, скорее всего, не имеют опыта разработки на языках с поддержкой Design By Contract (в первую очередь на Eiffel-е, на который в данной теме и следует равняться).

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


В разделе "3.2 Adoptability in legacy code" есть интересный заход: