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

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

[prog.thoughts] Так как же ИИ может изменить профессию "программист"?

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

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

Во-первых, это экономическая и политическая обстановка в мире. Если не будет какого-то катаклизма, сравнимого с Первой или Второй Мировыми войнами, если мировая экономика выберется из кризиса и вернется экономический рост, то у программистов в целом все будет ОК. Будет работа, которая, наверняка, сильно изменится. Но эта самая работа хотя бы будет. А вот если мир скатится во что-то вроде Великой Депрессии в США 1930-х годов, или периода Веймарской республики начала 1920-х годов, то увы. Всем будет очень и очень не очень. В том числе и программистам. А может быть программистам как раз в первую очередь.

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

Еще, конечно же, интересный вопрос в том, до какого уровня качества этот самый ИИ в итоге дойдет. Пока все говорят (говорят!), что ИИ с каждой новой версией становится все лучше и лучше. Хотя внешней публике и не видно во что (по деньгам инвесторов) эти улучшения обходятся.

А то ведь может оказаться, что с ИИ такая же ситуация, как с аудиофилией: очень несложно и совсем не дорого дойти до 90% от теоретически возможного качества звука. Зато стоимость каждого следующего процента качества возрастает в геометрической прогрессии. Может и с ИИ так? Может мы еще даже до 90% не дошли. И каждая следующая ступень качества, когда модели будут выдавать более точные решения и меньше галлюцинировать, будет обходиться на порядок дороже предыдущей. Чтобы в итоге стало не выгодно вкладывать деньги в повышение этого самого качества.

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

Думаю, что тогда наша профессия изменится. Быстро и бесповоротно.

Но не исчезнет.

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

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

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

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

Ну вот какие-то такие у меня мысли на данный момент. Однако, прошу не считать все вышесказанное инвестиционной рекомендацией.

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

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

[prog.thoughts] Надобность в "инфраструктурниках" из-за того, что людям приходилось писать код самим

Продолжу то, что начал ранее.

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

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

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

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

Получается, что чем более сложную задачу нам нужно решить, тем большим количеством более мощных абстракций мы должны располагать. Будь то языковые конструкции (вроде классов или шаблонов/дженериков) или библиотеки готовых "компонентов". А воплощение в жизнь подобных абстракций -- это именно что работа инфраструктурных программистов. Кто-то из инфраструктурщиков развивает языки программирования для того, чтобы поднять уровень и дать возможность выражать в коде все более и более сложные вещи, при этом обеспечивая и надежность, и удобство (если не брать в рассмотрение C++, конечно же). Кто-то пишет библиотеки и фреймворки. Кто-то создает вспомогательные инструменты вроде flex/bison или ANTLR.

В итоге прикладные программисты получают в свое распоряжение все больший и больший набор средств. Что дает возможность писать все более и более объемные и сложные программы. Делать это быстрее, с меньшими усилиями и с более предсказуемым результатом (если не брать в рассмотрение C++, конечно же).

Или может быть лучше сказать "получали"?

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

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

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

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

Это люди могли набить шишки на return -1, прийти к выводу, что исключения надежнее, потом собрать кучу граблей с исключениями, прийти к выводу, что нужен симбиоз, изобрести синтаксис `?` в Rust и try в Swift...

Чтобы в итоге этих самых людей заменил ИИ, который будет генерить простыни Go-шного кода с примитивнейшим `if err != nil` ничуть не страдая от того, что это и многословно, и отвлекает внимание, и не обеспечивает достаточной надежности.


Продолжение.

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

[prog.thoughts] Мое деление программистов на "прикладных" и "инфраструктурных"

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

Прикладные программисты делают то, что нужно пользователю. Причем не важно, о каком именно продукте идет речь -- это может быть какая-то сделанная на коленке под заказ система складского учета для провинциальной конторы "Рога и Копыта", пакет Microsoft Office или же сервер реляционной СУБД. Суть в том, что есть продукт, есть требования к нему, есть список фич, которые нужно выкатить, есть сроки, есть бюджеты, есть обязательства и т.д., и т.п. И появляется конкретный продукт для конкретного пользователя именно благодаря труду прикладных программистов.

Инфраструктурные программисты делают инструменты, которые используют прикладные программисты в своей работе. Самыми яркими примерами таких инструментов являются фреймворки и библиотеки. Как общего назначения (вроде Qt, JDK или .NET Framework), так и какие-то специализированные, заточенные под конкретный проект.

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

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

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

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

Яркий пример, демонстрирующий разность в подходах, довелось увидеть буквально пару дней назад в докладе «Опыт перехода проекта „Авито.Доставка“ с Java на Go» / Илья Лапин, Сергей Поляков (Avito), который мне YouTube зачем-то подсунул в рекомендациях (но оказалось довольно любопытно, благо доклад короткий и толковый):

Суть в том, что на момент доклада (2018-й год) в стандартной библиотеке Go (как и в самом языке) не было такого понятия как "множество" (оно же Set). Понятие "словарь" встроенное в сам язык было (в виде штатного типа map), а вот "множества" -- нет.

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

Тогда как инфраструктурный программист от подобного впадает в ступор. Типа: "а почему этот самый Set нельзя было взять и сделать?" 😉

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


Продолжение.

вторник, 28 июля 2026 г.

[prog.thoughts] И еще одна мысль про влияние ИИ на программирование

Навеяно вот этой статьей: Английский вместо кода

Когда я приходил в профессию на программиста возлагались следующие задачи:

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

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

Никаких бизнес-аналитиков и тестировщиков в то время в наших палестинах еще не существовало как класса, появилось все это лет на 5-10 позже (хотя на Западе, как я слышал, к началу 1990-х все это уже было). Ну да не суть.

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

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

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

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

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

Как по мне, так это уже и не программирование вовсе.

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

[prog.thoughts] Подумалось тут про ИИ и будущее программирования...

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

В связи с этим подумалось, что наступает золотая эпоха тех специалистов, которые были вынуждены программировать ну хоть как-то. Вроде физиков, химиков и биологов, которым нужно было делать вычислительные эксперименты, для чего приходилось осваивать программирование хотя бы в минимальной степени. В лучшем случае на Python или Pascal, но скорее на Fortran, а в особо запущенных случаях и на С++.

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

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

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

пятница, 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.

понедельник, 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" есть интересный заход:

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

[prog.c++] Похоже, что SObjectizer-5 будет использоваться в еще одном проекте

Больше двух лет сотрудничаю с интересным проектом. Занимался в рамках этого проекта разными задачами, начинал вообще с замены C++ REST SDK на RESTinio, а потом пошло поехало по нарастанию сложности 😎. Не все из этого было связано с многопоточностью, но многое.

Многопоточность здесь самая обычная -- std::thread, std::mutex, std::condition_variable, немного std::atomic-ов. Ну и специфика больше про параллельную обработку данных, нежели про событийно-ориентированное программирование.

Местами об отсутствии SO-5 в проекте приходилось жалеть, но, по большому счету, всерьез только один раз. Мне кажется, что та задачка на агентах решалась бы проще, чем на std::thread. Плюс еще пара-тройка мест, где простой агент с периодическим сообщением, на мой взгляд, оказался бы практичнее, чем выделенная нить с ручным циклом вокруг std::this_thread::sleep_for. Т.е. в недавнем прошлом от SO-5 полезный выхлоп вряд ли был заметным.

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

Это не точная формулировка задачи, но некоторое вполне уместное приближение. Делать это на больших объемах данных в один поток грустно по скорости, а распараллелить не так-то и просто (есть нюансы, в которые не могу вдаваться).

Такое распараллеливание средствами Taskflow было сделано, но некоторый осадочек остался:

  • в Taskflow нет встроенной поддержки таймеров. Одна из идей о том как оптимальнее поделить общий объем работы на отдельные кусочки базировалась на том, чтобы контролировать процесс через некоторые интервалы времени. Типа начнем считать на текущем треде, но поставим отложенную на 250ms задачу. Если к моменту ее запуска вычисление не завершится, то задача возьмет на себя часть оставшейся работы. Если же вычисление успеет закончится, то надобность в отложенной задаче отпадет. Только вот провалидировать эту идею из-за отсутствия таймеров в Taskflow сходу не получилось;
  • не был понятно как в Taskflow реализовать квотирование имеющихся ресурсов для того, чтобы одно вычисление не захватило все ресурсы себе, а оставшиеся вычисления сидели бы на "голодном пайке". При этом на горизонте маячит фича по приоритетам вычислений, т.е. каким-то высокоприоритеным вычислениям такая узурпация ресурсов разрешается, а вот низкоприоритетным -- нет.

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

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

Или, к примеру, запускается какая-то длительная операция (например, выгрузка части данных на диск). Сейчас для этой операции стартует отдельный тред, на котором операция выполняется, после чего тред завершается. Но плохо то, что таких тредов в какой-то момент может стать слишком много. Лучше бы их как-то координировать. И тут кажется, что сделать такого координатора на базе одного агента-менеджера и нескольких агентов-исполнителей несколько проще, чем посредством некой общей структуры данных, защищенной mutex-ом.

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

Не знаю, приживется ли SO-5 в проекте окончательно. Развитие идет динамично и у команды очень легкое отношение к включению или изъятию тех или иных зависимостей. Это я с большой осторожностью отношусь к тому что включать, а что нет, и могу долго раздумывать над тем, можно ли обойтись без какой-нибудь внешней библиотеки. Но здесь ребята более шустрые и рисковые -- если подвернулось что-то потенциально полезное, то сходу добавили. Если не оправдало себя, то не менее быстро выбросили 🙂

Так что может быть и SO-5 через месяц-другой-третий постигнет участь Taskflow. Поживем -- увидим. Сам я к этому отношусь спокойно: будет в проекте SO-5 -- хорошо, не будет -- не страшно. Даже если в итоге от SO-5 откажутся, то это даст еще больше полезной информации о том, где SO-5 применим, а где не очень.

Пока же я рад происходящему и есть два воодушевляющих фактора:

Во-первых, появляется дополнительный стимул изыскать время и ресурсы, чтобы возобновить дальнейшую работу над SObjectizer-ом. Признаюсь честно, что в последние 3-4 месяца с этим были проблемы, не хватало мне сил после основной работы находить еще по 2-3 часа в день, чтобы, скажем, вернуться к поддержке короутин в SO-5.

Во-вторых, SO-5 будут изучать и пробовать в работе совершенно новые люди. Наверняка от них последуют какие-то замечания/соображения, которые позволят сделать SObjectizer еще лучше. Да и вообще опыт применения SO-5 в новом проекте лишним точно не будет.

Так что будем пробовать и смотреть что из этого получится.

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

[prog.c++] Попробовал познакомиться с модулями C++20 и чего-то недопонял

Провел пару простых экспериментов с модулями C++20 и получил странные результаты.

Эксперименты проводились под Windows с VS2022 и VS2026 (обновления от мая 2026-го) и ArchLinux с GCC 16.1 и clang 22.1.

Ожидаемые мной результаты (т.е. отсутствие проблем компиляции/линковки) получились только с clang 22.1 и libc++. А вот с GCC и VC++ случились какие-то проблемы, которые мне сложно объяснить.

Во всех случаях сборка осуществлялась через CMake и Ninja.

Исходные коды описанных ниже тестовых программ можно найти в этом репозитории.


Эксперимент первый (case_001 из упомянутого репозитория). Очень простой модуль. Декларация в файле hello.ixx:

вторник, 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.

пятница, 8 мая 2026 г.

[prog.c++] Хочется странного, но теперь уже от std::map

В std::map начиная с C++17 есть отличный метод try_emplace. Он особенно хорош, когда конструирование mapped_type очень дешевое. Например, когда в качестве ключа у нас int, а в качестве значения -- структура с несколькими int-ами внутри. Тогда получается эффективно: попробовали вставить, если ключа в map еще нет, то из параметров сконструировали mapped_type и добавили в map новое значение. А если же в map ключ уже есть, то передача в try_emplace нескольких int-ов как параметров для конструктора mapped_type -- это копейки, на которые во многих случаях можно просто не обращать внимания.

Но вот когда у нас в качестве mapped_type какой-то "тяжелый" объект, вот тогда ситуация грустнее. Например, mapped_type -- это std::unique_ptr с указателем на класс с кучей собственных контейнеров внутри.

Если мы напишем что-то вроде:

auto [it, inserted] = my_map.try_emplace(key, std::make_unique<heavy_object>(...));

то это явная пессимизация -- ведь нам придется создавать heavy_object при каждом обращении к try_emplace, даже если ключ в map уже есть.

Я вижу два стандартных пути выхода из этой ситуации.

Во-первых, мы можем пойти классическим способом, через find с последующим emplace, если find завершился неудачно:

auto it = my_map.find(key);
if(it == my_map.end()) {
  it = my_map.emplace(key, std::make_unique<heavy_object>(...)).first;
}
// Теперь it указывает на объект внутри std::map.

Но здесь плохо то, что для вставки объекта поиск по std::map нужно будет делать дважды.

Во-вторых, в try_emplace можно передать пустой unique_ptr, а сам объект создать уже после вставки. Т.е. что-то вроде:

auto [it, inserted] = my_map.try_emplace(key, std::unique_ptr<heavy_object>{});
if(inserted) {
  // Была вставка. Теперь у нас в my_map лежит нулевой указатель, нужно
  // это исправить.
  it->second = std::make_unique<heavy_object>(...);
}

Но здесь плохо то, что нам нужно позаботиться об exception safety, ведь вызов make_unique может бросать исключение. И самое худшее, что можно сделать, это написать что-то вроде:

auto [it, inserted] = my_map.try_emplace(key, std::unique_ptr<heavy_object>{});
if(inserted) {
  // Была вставка. Теперь у нас в my_map лежит нулевой указатель, нужно
  // это исправить.
  try {
    it->second = std::make_unique<heavy_object>(...);
  }
  catch(...) {
    // Удаляем только что вставленный пустой указатель.
    my_map.erase(it);
    throw;
  }
}

Гораздо лучше было бы иметь что-то вроде scope(failure) из D. Что-то вроде:

auto [it, inserted] = my_map.try_emplace(key, std::unique_ptr<heavy_object>{});
if(inserted) {
  // Была вставка. Теперь у нас в my_map лежит нулевой указатель, нужно
  // это исправить.
  // Защищаемся от исключений.
  auto guard = at_failure(
    // Эта лямбда будет вызвана если выход из скоупа произойдет
    // из-за исключения.
    [&it, &my_map]() {
      my_map.erase(it);
    });
  it->second = std::make_unique<heavy_object>(...);
}

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

auto [it, inserted] = my_map.try_emplace(key, std::unique_ptr<heavy_object>{});
if(inserted) {
  // Была вставка. Теперь у нас в my_map лежит нулевой указатель, нужно
  // это исправить.
  // Защищаемся от исключений.
  do_with_rollback_on_exception(
    // Что хотим сделать.
    [&it, ...]() {
      it->second = std::make_unique<heavy_object>(...);
    },
    // Эта лямбда будет вызвана если первая лямбда бросит исключение.
    [&it, &my_map]() {
      my_map.erase(it);
    });
}

Но все эти приседания были бы не нужны, если бы был вариант try_emplace, который бы принимал не аргументы для конструктора mapped_type, а фабрику, которая может породить экземпляр mapped_type для вставки:

auto [it, inserted] = my_map.try_emplace_with_factory(key,
  // Эта лямбда будет вызвана, если объекта в map нет.
  [...]() {
    return std::make_unique<heavy_object>(...);
  });

К сожалению, такого варианта try_emplace_with_factory в std::map нет.

PS. Вышесказанное относится и к std::unordered_map.


Upd. В обсужении на LinkedIn посоветовали обходной маневр вида:

#include <map>
#include <string>

class simple_data {
    std::string _data;
public:
    simple_data(const char * s) : _data{ s }
    {}
};

class data_holder {
    std::string _data;
public:
    template<typename... Args>
    data_holder(Args && ...args) : _data{ std::forward<Args>(args)... }
    {}
};

template<typename F>
struct deferred {
    F _f;

    template<typename T>
    operator T() { return _f(); }
};

int main()
{
    std::map<int, simple_data> m1;
    m1.try_emplace(0, deferred{ []{ return simple_data{ "Hello, world" }; } });

    std::map<int, data_holder> m2;
//    m2.try_emplace(0, deferred{ []{ return std::string{ "Hello, world" }; } });
    m2.try_emplace(0"Hello, world");
}

Но не для всех случаев он будет работать. В частности для data_holder-а не сработает, т.к. у data_holder-а есть шаблонный конструктор (цынк).

понедельник, 4 мая 2026 г.

[prog] Любопытное из книги "C++ Ultra-Low Latency: Multithreading and Low-Level Optimizations"

Попалась в руки книга "C++ Ultra-Low Latency: Multithreading and Low-Level Optimizations". Начал ее листать, т.к. темой низкоуровневых оптимизаций на C++ никогда не занимался. Мне всегда было интересно писать корректно работающий код, который был бы понятным и сопровождабельным, который бы было просто использовать правильно, но сложно неправильно, но в плане скорости работы кода никогда не упарывался. В общем, как однажды сказали про мой код: "получение гарантий корректности времени компиляции при этом не используя Haskell" 🙂

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

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

Дело в том, что для меня всегда наиболее естественно было писать в стиле:

if(some_condition) {
  ... // тут много строчек кода с выполнением основной логики.
  ...
  ...
}
else {
  return some_error_code;
}

Т.е. большинство действий сосредотачивается именно в ветке then.

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

if(!some_condition) {
  return some_error_code;
}
else {
  ... // тут много строчек кода с выполнением основной логики.
  ...
  ...
}

Или даже вот такой:

if(!some_condition) {
  return some_error_code;
}

... // тут много строчек кода с выполнением основной логики.
...
...

Но оба эти стиля мне не нравятся на каком-то интуитивном уровне. Особенно последний (про этот стиль я уже высказывался: например, в контексте языка Go). Хотя, если мы в проекте придерживаемся принципов defensive programming, то начало метода/функции из if-ов для проверки входных параметров/состояния объекта, т.е. что-то вроде:

int f(int a, int b, int c) {
  if(a < 0) return invalid_parameter_a;
  if(b < 10 || b > 100) return invalid_parameter_b;
  if(c > 1000) return invalid_parameter_c;

  ... // Далее основная логика.
}

то такие короткие if-ы -- это нормально. Но когда проверки входных данных завершены и идет основной код метода/функции, то if-ы с короткими then или if-ы, в которых только return, на мой взгляд, ухудшают код (хуже только циклы, внутри которых короткие if-ы с continue).

И вот листая книгу "C++ Ultra-Low Latency: Multithreading and Low-Level Optimizations" вдруг натыкаюсь на подтверждение того, что привычный для меня способ написания if-ов имеет под собой обоснование еще и с точки зрения эвристик компилятора по обеспечению branch predictions.

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

[prog.c++] Просмотрел чужой доклад о SObjectizer

Марко Арена сделал доклад о SObjectizer и этот доклад публично доступен на YouTube: [Milan Meetup] Concorrenza multiparadigma con SObjectizer (Marco Arena)

Выступление на итальянском языке, но с помощью Yandex Browser можно послушать в переводе.

Дополнительные материалы (слайды на английском и репозиторий с кодом) доступны здесь: https://lao.bz/mcs

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

Про себя могу сказать так: с трудом верится, что все это происходит. Выкладывая SObjectizer в открытый доступ мы, конечно же, надеялись, что инструмент окажется кому-то полезным. Но вот чтобы доклады о SO-5 читал кто-то не из моей команды... Это всегда воспринималось как фантастика.

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