суббота, 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 в текстовом виде.

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

[prog.flame] Узнал давеча о переписывании какого-то Bun с какого-то Zig на какой-то Rust :)

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

Некий проект Bun (говорят, что это какой-то очень и очень быстрый инструментарий для JavaScript, включая и виртуальную машину) внутри Antropic-а переписали с Zig на Rust посредством нескольких десятков параллельно работавших в течении 11 дней агентов: https://bun.com/blog/bun-in-rust

На рассказ об этой процедуре среагировал разработчик языка Zig и высказался в том духе, что баба с возу кобыле легче переписали на Rust и славно, а то эти Bun-овцы уже изрядно задолбали дискредитацией всего Zig-а низким качеством своего кода: https://andrewkelley.me/post/my-thoughts-bun-rust-rewrite.html

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

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

fn foo(a_ptr: SharedPtr(TCPSocket)) !void {
  const a: *TCPSocket = a_ptr.get();
  defer a_ptr.deref();

  const b = try do_something_with_a(a);
  defer b.deref();

  // ...
}

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

PS. Так же я не полностью согласен с тезисом, что для C++ надежность кода достигается за счет следования какому-то административно утвержденному code style (как в случае с упомянутым в блог-посте от Bun-овцев Google C++ code style guide). Многие проблемы (вроде контроля времени жизни объектов, находящихся в совместном использовании) должны решаться не соглашениями, а типами (вроде unique_ptr, shared_ptr и т.д.) Но кто я такой, чтобы рассуждать на эту тему? 😉

PPS. А Rust подтверждает свою репутацию языка, на котором ничего нового не пишут, а только переписывают существующее 🤣