Обновить

Вы все еще пишете многопоточку на C++ с ошибками синхронизации?

Уровень сложностиСредний
Время на прочтение11 мин
Охват и читатели29K
Всего голосов 53: ↑47 и ↓6+57
Комментарии149

Комментарии 149

ЗакрепленныеЗакреплённые комментарии

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

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

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

Даже в этом случае есть решения. Например данные часто читаются: read write locks, copy on write и т.д.

Ну так есть и lock-free варианты, и всякие спинлоки, есть уж данные так быстро изменяются.

Но у меня тут более общий вопрос. Если некий набор данных охраняется одним локом/мьютексом, то эти данные взаимозависимы, то есть, изменение одной переменной должно обычно привести к изменению другой переменной, иначе зачем их охранять одним общим локом? Тогда нет смысла лочить каждое отдельное изменение отдельной переменной, а имеет смысл создавать явные методы, которые будут связно изменять все зависимые переменные внутри одного блока lock-release. И тогда данный механизм автора не подходит. А подходит он только для структур с одним членом.

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

Хмм..

Сейчас посмотрел код ещё раз и вроде вы правы, внутри _modify можно менять все связанные переменные. Это я съел чего-то)

Вы все еще пишете многопоточку на C++ с ошибками синхронизации?

Уже давно нет. И многократно рассказывали как это делать.

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

Да и в озвученном подходе с SharedState стоит только ссылке на данные из лямбды протечь куда-нибудь, как приключения не заставят себя ждать.

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

Хорошо хоть ссылки оказались полностью из ASCII, а то бы щас пол статьи в процентах наблюдали.

Проблемы с синхронизацией обнаруживаются даже при работе с акторами

Где можно про это почитать?

ХЗ. Я говорю исходя из личного опыта.

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

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

Это вы сейчас эрланг придумываете?

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

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

НЛО прилетело и опубликовало эту надпись здесь

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

НЛО прилетело и опубликовало эту надпись здесь

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

Т.е. можно будет относительно безболезненно отстреливать приболевшие части сервиса в виде его отдельных процессов, не пытаясь починить их погулявше поврежденные heap/stack и прочие local state.

Вот поэтому мне решение с параллельными процессами (заданиями - job) нравится больше чем решение с нитями-потоками (thread). Оно легче сопровождается и более устойчиво к сбоям в силу изолированности отдельного задания относительно всех остальных.

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

P.S. Раз статья вам не пригодилась вежливость требует возвратить вам лучи известной субстанции от благодарного автора :)

Ну а почему, собственно, вопрос дурацкий?

а) это кликбейт в чистом виде;
b) претензия на то, что в статье будет описана вот прям "серебряная пуля".

Если вы давно уже нет, поздравляю от всей души, но позволю усомниться такому самоуверенному заявлению :)

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

И, во-вторых, вот это:

Раз статья вам не пригодилась вежливость требует возвратить вам лучи известной субстанции от благодарного автора

Я не говорил о полезности статьи вообще. "Благодарность" была высказана исключительно за примеры кода в виде картинок. Но вы, в силу своеобразного восприятия реальности выдумали себе ХЗ что.

Про Kaspersky OS можно и не беспокоится. Она явно в надежных руках.

a) название статьи должно быть кликабельным

b) "серебрянной пули" нигде не обещал, вы это сами себе придумали, видимо, из-за упомянутого здесь нестандартного воcприятия реальности :)

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

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

> Вы же этот дурацкий ответ воспринимаете всерьез.
Да нет же, с чего вы взяли? :) Ну как можно всерьез отвечать собеседнику, который пишет ХЗ что про лучи и какие-то субстанции? :)

> Я не говорил о полезности статьи вообще. "Благодарность" была высказана исключительно за примеры кода в виде картинок.
Да я понял, конечно же :) Тут был вопрос к корректности высказывания своих замечаний к статье.

> Про Kaspersky OS можно и не беспокоится. Она явно в надежных руках.
А вы почитайте на досуге про KasperskyOS - скептицизма сразу поубавится :)

a) название статьи должно быть кликабельным

Кликабельность != кликбейтность

b) "серебрянной пули" нигде не обещал

В том и суть: заголовок претендует на что-то серьезное, а по факту получается такое себе.

Ну как можно всерьез отвечать собеседнику, который пишет ХЗ что про лучи и какие-то субстанции?

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

Если суть претензии для все все еще "ХЗ", то хотя бы обратите внимание на:

  • количество людей, поддержавших мой комментарий;

  • я не единственный, кто такую претензию озвучил.

Можно и дальше считать меня идиотом, а можно просто сделать выводы.

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

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

Кликабельность != кликбейтность

Так это зависит от субъективной оценки - в моем понимании кликабельный, в вашем кликбейт.

В том и суть: заголовок претендует на что-то серьезное, а по факту получается такое себе.

Опять же ваша субъективная оценка.

Вы, похоже, никогда не слышали выражения "посылать лучи поноса"

Дело не в том слышал или нет, просто не считаю это уместным и корректным для высказывания замечания.

Если суть претензии

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

Можно и дальше считать меня идиотом

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

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

"лучи поноса" не адекватно воспринял? Ну уж извините :)

Так это зависит от субъективной оценки - в моем понимании кликабельный, в вашем кликбейт.
Опять же ваша субъективная оценка.

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

Дело не в том слышал

Если слышали, но все равно завели речь про "Ну как можно всерьез отвечать собеседнику, который пишет ХЗ что про лучи и какие-то субстанции?", то тут уж либо крестик снимите, либо...

просто не считаю это уместным и корректным для высказывания замечания.

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

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

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

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

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

если, конечно, хотите чтобы к нему прислушались.

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

Я-то, по наивности, полагал, что обратная связь в первую очередь нужна автору статьи. Но, видимо, сильно ошибался.

PS. Если позволите, нескромный вопрос: а в Kaspersky Lab вы попали обычным образом -- через серию собеседований, включая знание языка, алгоритмическую секцию и т.д.?

Ну понятно: есть два мнения -- одно из них мое, второе неправильное.

Есть много мнений: с какими-то соглашаешься с какими-то нет, а у вас только два? Или это вы за меня решили порассуждать? :)

Можно принять это несовпадение к сведению

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

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

Не на совете, но и не в подростковом чате :) мем мему рознь.

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

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

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

Да ладно вам :) адекватную конструктивную критику интересно послушать.

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

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

Я-то, по наивности, полагал, что обратная связь в первую очередь нужна автору статьи. Но, видимо, сильно ошибался.

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

PS. Если позволите, нескромный вопрос: а в Kaspersky Lab вы попали обычным образом -- через серию собеседований, включая знание языка, алгоритмическую секцию и т.д.?

Да, пожалуйста. Все верно так и проходил. У меня тогда тоже нескромный встречный вопрос: а вы случайно не состоите на учете в ПНД? :)

совсем другое когда сразу начинают писать про "поносные лучи" :) диалог получается из этой же серии

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

За обилие смайлов в ответных комментариях еще одна порция оных.

Да, пожалуйста. Все верно так и проходил.

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

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

Ну это же были не те примеры, которые есть смысл собирать и что-то менять в них. Это скорее иллюстрация описываемой проблемы. Реализация и примеры использования приводятся в конце статьи как ссылки - там можно "пощупать" реальный код.

Что и было сделано в мягкой форме.

Страшно представить как вы это делаете в жесткой форме :) бедный монитор.

За обилие смайлов в ответных комментариях еще одна порция оных.

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

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

Так вы еще и HR специалист оказывается, а как же нужно делать, по вашему мнению?

Например, Streams в Java 8 сделан удобно — достаточно добавить один вызов, и вся коллекция уже обрабатывается в параллели, а разработчику не нужно думать об этих низкоуровневых сущностях.

Только все streams в приложении по умолчанию используют общий fork-join thread pool, и у вас из-за этого может быть много неприятных сюрпризов, если какой-то stream, например, случайно заблокирует все потоки в fork-join pool. Так что тут тоже приходится думать о низкоуровневых сущностях.

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

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

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

Не первый раз вижу этот код и снова повторю, во-первых, подход заставляет компилировать вместо N типов данных и K мьютексов все N * K пар мьютекс - данные, но это самая малая проблема.

Ещё такой подход сильно провоцирует гонки апи, когда взяли лок и достали например .empty() у вектора, а дальше сделали лок и .pop_back, а вектор уже пуст. В общем не получится у вас "не думать" когда пишете код

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

Ну и наконец самая главная проблема, что за чертовщина на уровне реализации? Зачем все эти std::function, std::condition_variable_any., прости господи .template extract<packaged_task<....>>? Что с интерфейсом, откуда тут взялись when и подобное? Это что, фьючи?

Дальше, ещё конкретнее про реализацию. Совершенно неочевидные наборы перегрузок:

    inline void view(std::function<void(const T&)> block) const {
        LockRead lock(_mutex);
        block(_state);
    }

    template<typename R>
    inline R view(std::function<R(const T&)> block) const {
        LockRead lock(_mutex);
        return block(_state);
    }


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

return (R)block(_state) это сработает и с void с и другими типами

https://godbolt.org/z/59cazh9dE

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

Тут вообще большой вопрос -- а зачем тянуть std::function для block-а? Почему бы не сделать так:

template<typename BlockLambda>
decltype(auto) view(BlockLambda && block) const {
  LockRead lock(_mutex);
  return block(_state);
}

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

В боевом коде, безусловно, лучше сделать как у вас.

Т.е. ваш код, на который вы ссылаетесь, он не "боевой"? Это типа вы C++ изучали и в процессе изучения экспериментировали для души?

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

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

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

Как я уже написал выше, класс SharedState полностью работоспособный и покрыт UT. Напильником ее дорабатывать не нужно, но можно при желании, потому что совершенству не предела :)

Как я уже написал выше, класс SharedState полностью работоспособный

Ну да, ну да. А вот здесь ув.тов@Kelbonn вовсе не косяк в вашей реализации нашел.

Косяк? А вы вообще читали что там написано? :) В чем по вашему заключается косяк?
Человек в строке 27 явно вызывает версию метода view без шаблона:
`v.view([] (int i) -> int {`

Если попробовать присвоить возвращаемое значение переменной код перестанет компилироваться:
`auto d = v.view([](int i) -> int {`
Потому что вызывается версия view без шаблона, которая возвращает void.

Если нужно возвращать, например, int нужно пользоваться версией view с шаблоном:
`auto d = v.view([](int i) -> int {`

Тут даже коментировать особо нечего, думал человек сам осознает :)

Если нужно возвращать, например, int нужно пользоваться версией view с шаблоном:`auto d = v.view<int>([](int i) -> int {`

Полагаю, вы просто не поняли о чем вам сказали.

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

Почему вы так полагаете?

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

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

Он вам не на это указывает.

Вот пример для проверки вашего подхода:

#include <iostream>
#include <functional>

struct demo
{
    long long m_delta;

    template<typename T>
    auto apply(T v) const { return v + m_delta; }
};

template<typename T>
struct wrapper
{
    T m_value;

    void view(std::function<void(const T&)> fn) const {
        fn(m_value);
    }

    template<typename R>
    R view(std::function<R(const T&)> fn) const {
        return fn(m_value);
    }
};

template<typename T>
void accept_and_show(const char * case_name, T && v)
{
    std::cout << "=== " << case_name << " ===" << std::endl;
    std::cout << "T: " << typeid(T).name() << std::endl;
    std::cout << v << std::endl;
    std::cout << "===" << std::endl;
}

int main() {
    wrapper<demo> w{ demo{13} };

    long l{ 55 };
    accept_and_show("l+delta",
        w.view([l](const auto & delta) { return delta.apply(l); }));

    char c{ 33 };
    accept_and_show("c+delta",
        w.view([c](const auto & delta) { return delta.apply(c); }));
}

Можно обратить внимание на то, что demo::apply возвращает auto.

Попробуйте сделать так, чтобы этот код скопилировался. И не был захардкожен на текущую реализацию demo (чтобы было понятнее: код должен компилироваться даже если тип demo::m_delta будет заменен на какой-то другой (вроде short, float, double или даже любой другой пользовательский тип с поддержкой operator+).

У меня, например, получилось вот так: https://wandbox.org/permlink/WumQM0PTODKQKRwL

В тоже время: https://wandbox.org/permlink/ev55r0kfjP0R1fzI

Любой класс с двумя шаблонами N и K может сгенерировать N * K вариантов, если видите в этом проблему, тогда конечно, видимо, лучше не пользоваться шаблонными классами :)

Гонками обычно называют ситуации когда два потока "на перегонки" одновременно меняют общие данные и в итоге приводят их в несогласованное состояние. При правильном использовании мьютексов или как здесь предлагается SharedState вместо них, гонки как раз исключаются.

Писать код "не думая" не думал даже рекомендовать - вы, видимо, как-то превратно трактуете прочитанное :)

А где вы увидели мьютексы в публичном API - они наоборот спрятаны внутри SharedState, который тоже совсем необязательно светить в публичном API, это скорее предполагается как часть внутренней реализации.

Насчет "чертовщины", пожалуйста, в церковь :)

Из личной практики. Очень часто мне не требуется именно работа с какими-то общими данными из нескольких потоков. Чаще задачи двух классов -

  • "конвейерная обработка потока данных"

  • "параллельная обработка большого количества независимых элементов".

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

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

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

Параллельная обработка - когда есть много (десятки миллионов и более) однотипных элементов и каждый из них нужно независимо от остальных обработать по какому-то алгоритму (может быть достаточно сложный и долгий). Тут подход иной - есть "головное задание", которое формирует пакеты элементов (например, делает выборку из БД по заданным условием, результаты выборки объединяются в пакет, скажем, по 100 элементов) и выкладывает их в очередь. Параллельно работают несколько экземпляров обработчиков, каждый из которых берет из очереди очередной пакет и обрабатывает содержащиеся в нем элементы.

В обоих случаях нет нужды связываться с разделяемой памятью и синхронизацией доступа к ней. Можно воспользоваться системными средствами (и за всю синхронизацию будет отвечать система). Для конвейерной обработки используется принцип "почтовых ящиков" - у каждого потока есть свой ящик (в Windows можно использовать mailslot, в иных системах - локальный именованный Unix socket) куда любой может писать блоки данных для этого потока.

При параллельной обработке можно использовать pipe в который головное задание пишет пакеты, а обработчики читают их оттуда. Ну или если истема поддерживатье что-то еще подходящее (сейчас вот с IBM i работаю - там есть очереди - data queue и user queue - ккк раз для такого удобно).

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

При параллельной обработке можно использовать pipe в который головное задание пишет пакеты, а обработчики читают их оттуда. Ну или если истема поддерживатье что-то еще подходящее (сейчас вот с IBM i работаю - там есть очереди - data queue и user queue - ккк раз для такого удобно).

Использу в качестве трнаспорта zeromq, в т.ч. для параллельной обработки ( см. замечательную книгу https://wikileaks.org/ciav7p1/cms/files/ØMQ - The Guide - ØMQ - The Guide.pdf)

Ну тут что есть под рукой :-) Пайпы в принципе очень просты в работе и не грузят систему.

Но у нас на платформе есть User Queue - системный объект (т.е. никаких библиотек - поддерживается системными средствами). Преимущество в том, что его не надо каждый раз создавать-удалять. Один раз при развертывании поставки создал (с нужным именем) и оно есть. Только подключайся. Оно может быть FIFO, LIFO или KEYED - когда каждый пакет еще дополнительно снабжается "ключом" и можно этот ключ использовать в качестве условия для извлечения сообщения (равно, не равно, больше, меньше, больше или равно, меньше или равно) - извлекается первое сообщение, подходящее под условие. Основное преимущество перед пайпом - есть возможность "материализации" - получения состояния очереди (в т.ч. максимально возможное количество сообщений и текущее количество сообщений) что позволяет контролировать скорость раздачи и разбора и динамически балансировать систему (если очередь растет - добавить обработчик, если уменьшается - остановить какой-то из обработчиков). А поскольку это системный объект, то даже в случае падения задания (головного, обработчиков) содержимое очереди сохраняется в памяти системы.

Есть еще Data Queue - примерно тоже самое, но более тяжелая за счет того, что хранит все содержимое свое на диске.

И та и другая очереди доступны как через API, так и через SQL

Хотим посмотреть информацию об очерелди

select *
  from table(USER_QUEUE_INFO('TSTQUE'));

Получаем

Хотим посомтреть содержимое (без удаления из очереди - "материализация сообщений", peek)

select *
  from table(USER_QUEUE_ENTRIES('TSTQUE'));

Получаем

Для сопровождения очень полезно

Тем, кто хочет по-прежнему использовать mutex, но не нарываться на проблему "захватили не тот mutex" или "не захватили вообще", могу порекомендовать аннотации из Abseil для проверок во время компиляции: https://abseil.io/docs/cpp/guides/synchronization#thread-annotations

Оккам точно в гробу перевернулся после таких модификаций...

Сущностей стало куда больше (даже в таком простом примере), код сложнее для восприятия, а при этом всё также остались "правила", которые нужно соблюдать, чтобы ничего не падало.

То есть задачу 100% эта методика не решила, а сложности добавила.

Предлагаю Оккама оставить в покое и посчитать на пальцах сущности до и после: были mutex, lock guard и condition variable + защищаемые данные, стало SharedState + защищаемые данные, которые могут быть отдельной структурой/классом или просто строкой, например. Т.е. мы уменьшили общее количество сущностей, которыми вынуждены были манипулировать и получили одну новую абстракцию, которая объединяет общие данные со средствами их защиты. А правила всегда будут чем бы вы не пользовались :)

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

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

Да, верное решение. Геттер/сеттер которые работают с данными, защищенными мьютексом.
Как это будет реализовано уже не суть важно - класс, лямбда... Важно что вы не обращаетесь к данным напрямую, только через get/set, а те уже внутри используют мьютекс.

Само по себе отсутствие гонок в геттере/сеттере ещё не гарантирует отсутствия гонок вообще и тем более общей корректности параллельного алгоритма.

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

Согласен. Посему лучше стараться выбирать те подходы, которые в принципе не предусматривают таких ситуаций. Если задача позволяет, конечно. Потому, как уже писал, стараюсь без нужды в такие блуды не лезть (в тех задачах, что приходится решать, в 99% случаев получается выбрать иные подходы).

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

НЛО прилетело и опубликовало эту надпись здесь

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

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

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

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

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

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

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

Что именно вам кажется ерундой? Контроль над данными с которыми осуществляется совместная работа несколькими потоками? Идея изоляции шареных данных?

Мне вот полной ерундой кажется когда при слове "многопоточка" сразу начинаются мьютексы и все вот это вот. Без привязки к конкретной задаче.

Я уже писал тут, что в зависимости от задачи можно выбирать иные подходы. И многопоточная обработка не всегда требует работы с одним массивом шареных данных. А если и требует - см. любую операционную систему где 10 программ могут работать с одним файлом, но все делают это через системное API (все ваши read/writeв конечном итоге приходят в одну точку в ядре системы). И все проблемы конкурентного доступа и блокировок решаются в одном месте - на уровне ОС. А системное API отдает в программы уже безопасную копию данных в их текущем состоянии.

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

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

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

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

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

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

Что именно вам кажется ерундой?

Попытки рассуждать вслух вокруг да около.

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

Да куда уж мне, я и программировать-то не умею. Это вам любой анонимный эсперт с LOR-а подтвердит.

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

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

Попытки рассуждать вслух вокруг да около.

А вы все задачи решаете однотипно? Без учета конкретики и граничных условий? Все копипастой старых решений? И никогда "на берегу" не задумываетесь "а что будет если..."?

Да куда уж мне, я и программировать-то не умею.

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

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

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

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

Вот приходит человек - "нужно сделать то-то, можно так, можно этак - как лучше?". И вот тут зависит от условий. Если это отдельный процесс, то есть одно решение - более простое в реализации и более эффективное. Но в случае актора, вызываемого одновременно из 100500 мест 100млн раз в стуки это решение потащит за собой много накладных расходов от системы и не даст стабильной производительности. Поэтому лучше выбрать другое, которое чуть сложнее в реализации и менее производительное в одном потоке, но зато стабильное в условиях большой плотности параллельных вызовов. Вот то, что я хочу услышать от опытного разработчика. А не умение решать ликодовские задачки и зачитывания наизусть произвольного места из последнего стандарта языка или перечисления всех классов стандартной библиотеки.

А вы все задачи решаете однотипно?

Я здесь вообще не при чем.

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

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

Но программирование и разработка, все-таки, немножко разные вещи.

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

Блин, вам корона не жмет, трон не высоковат? Только вы здесь настоящий эксперт с опытом.

И в данном конкретном случае (этой конкретной статьи) можно только абстрактно рассуждать.

Да что вы говорите?!!

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

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

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

Если вы в этом видите возможность только "абстрактно рассуждать", то у меня для вас плохие новости.

Но вам такие люди не нужны, вы ищете себе подобных

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

НЛО прилетело и опубликовало эту надпись здесь

Если вы в этом видите возможность только "абстрактно рассуждать", то у меня для вас плохие новости.

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

А вокруг выдернутого из контекста куска кода можно рассуждать только абстрактно. И до бесконечности оптимизировать то, что (возможно!) можно вообще в данном случае реализовать совсем иначе - проще и эффективнее.

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

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

Все!

Это же очевидно как не знаю что.

Какой здесь еще кому-то контекст потребовался... Ну вот, честно, ХЗ.

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

Здесь все предельно понятно: если у вас в одном объекте два мутекса и N полей, для К из которых нужно захватывать первый мутекс, а для остальных M полей -- второй мутекс, то высока вероятность ошибиться.

Глубочайшая мысль. Несомненно, требующая отдельной статьи.

Вопрос - там действительно надо делать именно так? Вот я о чем спрашиваю. Иных вариантов нет? Те же lock-free алгоритмы там не подходят?

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

Вот я о чем спрашиваю.

Скажите, а когда вы читаете документацию к std::lock_guard, вы тоже задаетесь вопросом: а там действительно нужно делать именно так?

Т.е. вот такой вариант синхронизации - это уже крайний случай.

"Отучаемся говорить за всех" (c)

НЛО прилетело и опубликовало эту надпись здесь

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

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

НЛО прилетело и опубликовало эту надпись здесь

А вот мне, пользователю библиотеки, при ее использовании - уже нет, я как юзер

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

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

Еще можно было бы понять, если бы мутексам в противовес приводили RCU или software transactional memory. Но высокопарные рассуждения про MDBX/SQL...

НЛО прилетело и опубликовало эту надпись здесь

Так это и предлагали. Прям именно это.

Простите, но где? MDBX, Berkeley DB, SQL, ACID -- этого в достатке. "Транзакционность памяти" касательно СУБД была.

А вот конкретно RCU или STM в ваших стенаниях что-то не замечал.

Что это?

Это то, что вы разводите в своих комментариях. Как, например, рассуждения про инженеров и мастеров-сборщиков.

Опять у кого-то пригорело?

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

НЛО прилетело и опубликовало эту надпись здесь

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

Да и вообще лучше быть богатым и здоровым, а не бедным и больным.

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

а судя по примерам из статьи речь идет о них

...видимо, вы увидели в статье что-то свое. Отсюда и растекание мыслею по древу.

все было придумано еще в 70-х.

Что-то меня терзают смутные сомнения, что в 1970-х уже были придуманы тот же RCU или, например, lock-free структуры данных.

НЛО прилетело и опубликовало эту надпись здесь

А там еще TBCC есть, timestamp based concurency control, она-же insert only database, она же Log Database, она же EventSourcing, она-же

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

Умных слов много, смысла мало.

НЛО прилетело и опубликовало эту надпись здесь

И к чему это было сказано?

К тому, что ваш бенефис знаний о подходах из СУБД в комментариях к данной статье является офтопиком. Может быть он был бы уместен, если бы статья пыталась сравнивать разные подходы к решению проблем конкурентности в C++(*). Но нет, она не о конкурентности вообще, она о том, как уменьшить вероятность выстрелить в ногу если mutex-ы все-таки нужны.

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

Еще одна ваша проблема в том, что вы рассуждаете о сферических конях в вакууме. Нужно нехило так пораскинуть мозгами, чтобы понять, как идею MVCC использовать для решения той или иной задачи, не связанной с хранением данных в БД. А вы не даете никаких намеков на то, как эти самые "только INSERT на каждое изменение" применять в решении тех или иных задач. Только демонстрация собственной эрудиции, не более того.

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

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

Ваши же рассуждения о том, как классно все в MDBX, никак не проливают свет на способы реализации подобных thread-pool-ов без mutex-ов.

Могу предположить, что вы здесь возразите, мол, прикладному программисту не нужно делать свои thread-pool-ы, а кому нужно, тот разберется. На что я позволю себе сделать два упреждающих ответа:

a) мы говорим про C++, здесь даже "прикладной" программист должен уметь написать свой thread-pool. Т.к. стандартных пока нет как класса, а условия у "прикладных" задач на C++ ну очень уж разные;

b) почему вы вообще решили, что речь идет о прикладной разработке? Вы выдумали себе какой-то тезис, не дали себе труда его как-то обосновать, но начали грузить читателей своей эрудицией из совсем другой области. Если вы настолько уверены, что известные вам принципы работы СУБД облегчают реализацию конкурентных приложений на C++, то напишите статью. Я думаю, такая статья будет интересна очень многим (включая меня).

--
(*) В C++ за счет отсутствия сборщика мусора и из-за отсутствия Rust-овского borrow checker-а проблем с конкурентностью больше. Например, реализация персистентных структур данных (в смысле работ Окасаки, а не в смысле сохранения значений во внешней памяти) из-за отсутствия GC более геморойна.

НЛО прилетело и опубликовало эту надпись здесь

Зачем для организации динамически расширяющихся тредпулов вообще нужны мутексы? Чушь какая-то...

То наверное да, без мутексов в пуллах потом не обойтись.

Так "не обойтись" или "чушь какая-то"

Вы уж определитесь.

И речь шла не про очереди заявок для thread-pool-а.

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

Ошибаетесь. Вам тут уже привели пример вашей же MDBX, в которой около 200 применений мутексов.

При чем тут принципы БД офтопик?

Подумайте.

С какой стати они вообще офтопик, если там все очень плотно на конкуретном доступе и реализовано?

Потому что там конкурентность для решения задач СУБД. Если создается, скажем, видеоредактор или прокси-сервер, то конкурентность и там потребуется, но вряд ли она будет такая же, как в случае с СУБД.

Что мутексы это круто и они очень хорошие и они всегда нужны?

Нет.

Ок, эту мысль зафиксировали.

Вы зафиксировали совсем не то.

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

Т.е. прикладник всегда должен видеть узкие места - где минимальными усилиями получить максимальный выигрыш в производительности. По всей задаче в целом.

НЛО прилетело и опубликовало эту надпись здесь

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

Multithreading -- это всего лишь инструмент для использования в двух принципиально разных областях:

  • parallel computing (яркий пример -- OpenMP), выигрыш от multithreading в том, что все в рамках одного адресного пространства без необходимости использования каких-либо средств IPC;

  • concurrent computing (яркий пример -- упомянутые вами blocking I/O). Опять же удобно то, что все в рамках одного адресного пространства.

И если в случае concurrent computing вопрос производительности дискуссионный и может быть даже принесен в жертву простоте/надежности реализации, то в случае parallel computing все гораздо очевиднее (опять же отсылка к OpenMP, а так же к инструментам вроде task-flow и HPX).

И заявлять о том, что multi-threading не имеет отношения к производительности даже не уточнив о какой именно области идет речь -- это "мощно, внушаить!" (c)

PS. Очевидно, что @SpiderEkb в своих комментариях выше говорил о parallel computing. При этом ему везло оказываться в ситуациях, когда расходы на IPC по сравнению с основной обработкой данных в его задачах были настолько малы, что ими можно было пренебречь.

НЛО прилетело и опубликовало эту надпись здесь

Чем минусы ставить - читать стоит сначала.

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

Там было сказано - обычно не имеет. Имелось в виду тех самых задачах concurrent computing и blocking I/O.

Только вот это "обычно не имеет" было сказано задолго до того, как вы про blocking I/O упомянули.

Вообще не очевидно.

Даже если просто читать внимательно, то уже очевидно. А если еще и хоть слегка в теме, то и подавно.

НЛО прилетело и опубликовало эту надпись здесь

Я так понимаю примеров эффективного решения задач с высококонкурентным параллелизмом на тредах (а не банальных MPP с тотальной изоляцией workerов и их окружения) мы не дождемся?

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

Мне интересен лишь один простой вопрос - насколько реально нужны именно треды с мутексами и можно ли их заменить на взаимно изолированные процессы в один поток с message passing, как универсальную архитектуру в общем случае.

Вам интересно и что? Почему на вас кто-то должен тратить свое время?

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

НЛО прилетело и опубликовало эту надпись здесь

Да, да, и офтопик с MVCC из СУБД тоже я, как и часовню.

Вы ведете себя как ребенок, поэтому и обращаться с вами приходится как с ребенком.

НЛО прилетело и опубликовало эту надпись здесь

Несмотря на то, что я разделяю, в определенной степени, ваши восторги относительно наличия MDBX и ее реализации (@yleo проделал колоссальную работу, за что ему огромное спасибо) - но:

  1. появляется понятие версионности данных не доступно на пользовательском уровне.

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

  3. Snapshot Isolation и Copy-on-Writer приводят к необходимости сборки мусора - что тоже не самая быстрая операция, а долгие читающие транзакции не рекомендуется делать

  4. Нужно просто доделать враппер над этой MDBX, чтоб он пользовательские объекты представлял с автоматическим маппингом в этои самые key-value  - это не просто, в том смысле что вам надо будет принять некоторые решения с точки зрения вашего API

  5. Книжек про то как реализовать MDBX или аналог действительно не очень много, но его API не требует каких-то больших усилий для использования. Концепция Snapshot Isolation очень сильно упрощает пользовательский код, до той степени, что есть ощущение, что пишешь однопоточный код. Но вопрос согласованности пользовательских данных лежит полностью на ваших плечах, а не MDBX.

  6. mutex в кодовой базе libmdbx встречается более 200 раз, наверное иначе не умеют даже там.

НЛО прилетело и опубликовало эту надпись здесь

Это все юношеский идеализм. Или максимализм. В общем попытка решить проблему силой, а не опытом.

Но опять-же, так мало кто делает, всем страшно (как так, отказаться от SQL базы, уууу, не по канонам-паттернам, засмеют, и резюме совсем подпортится).

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

Но тут речь идет о работе с памятью. Или вы про in-memory DB? Так есть альтернативы в виде lock-free алгоритмов. Те же конкурентные очереди, деревья, списки...

Как пример

Вопрос всегда в том - для вашей конкретной задачи это действительно нужно? Скорость доступа к данным для вас действительно является узким местом?

В моей практике таких задач было по пальцам пересчитать (за 30+ лет). В подавляющем большинстве случаев необходимость многопоточки была связана с большими затратам времени на обработку данных. Т.е. ускорив доступ к шаренным данным в 2-3 раза, я получу суммарный выигрыш в производительности всей системы в доли процента. Тут просто овчинка выделки не стоит - берем любой системный канал связи (пайпы, сокеты и т.п.) и легко и быстро реализуем на нем обмен данными между потоками/процессами. А конкурентность доступа уже обеспечивается системой.

НЛО прилетело и опубликовало эту надпись здесь

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

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

Вот конкретно сейчас в работе. Есть две БД. Одна содержит в пределе до 250млн строк, вторая (опять в пределе) до 1млн строк. Задача - найти "совпадения" по строкам из первой и второй БД. Совпадением считается вхождение всех уникальных элементов (слов) строки из второй БД в строку из первой БД. Т.е. строка из второй БД может быть короче строки в первой и порядок следования элементов там может отличаться. Например:

Строка 1: 'A B C C D E E'
Строка 2: 'E B C B'

Это совпадение т.к. все уникальные элементы 'B', 'C' и 'E' строки 2 содержатся в строке 1.

Каждое совпадение должно быть зафиксировано в таблице. У каждой строки в БД (1 и 2) есть некий идентификатор. В таблице нужно фиксировать что строка из БД2 с идентификатором ... совпадает со строкой из БД1 с идентификатором ...

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

Если просто в один поток - работать будет сутками (если что - 120 8-поточных ядер Power9 - не хрен собачий, извиняюсь). Так что распараллеливаем на несколько потоков (обычно от 5-ти до 10-ти - полностью загрузить сервер своей задачей никто не даст - там еще 100500 разных процессов крутится плюс загрузка сервера в нормальном режиме не должна превышать 50-60% т.к. бывают периоды пиковой нагрузки когда она доходит до 90% - должен быть запас).

Решается это распараллеливанием обработки. Есть головное задание - оно производит отбор элементов (идентификаторов строк), например из БД2, по заданным условиям (там не всегда надо отбирать все). Отобранные идентификаторы объединяются в пакеты (скажем, по 100 штук) и выкладываются в очередь для обработки. Параллельно головному заданию (мы работаем именно с заданиями - job - это удобнее для сопровождения и безопаснее - задания полностью изолированы и падение одного даже по самой страшной ошибке не затрагивают остальные) работает 5-10 задний-обработчиков. Обработчик берет из очереди очередной пакет и обрабатывает содержащиеся в нем элементы. После чего берет следующий и так пока очередь не опустеет.

Все время тут определяется временем обработки элемента. Все упирается в него. Затраты времени на транспорт вообще никак не влияют - будет это 1мкс или 1нс - не изменит ничего если время обработки одного пакета исчисляется десятками мс.

В качестве транспортной очереди используется то, что предоставляет система. Это может быть, например, pipe. Или, на нашей платформе, есть системный объект user queue простая в работе и быстрая очередь. Все блокировки чтение-запись, удаление прочитанного элемента - за все это отвечает система. На нашей стороне фактически две операции - enqueue (положить) и dequeue (взять). Все. Никаких мьютексов и прочего.

Если начать упираться в транспорт через расшаренную память - да, транспорт станет быстрее. Но на это потратится изрядно сил, а общий прирост производительности составит 0.000001%

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

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

Плохо что коммуникационные таймауты очень маленькие. Нужно быстро проверить полученное сообщение (формат совпал, CRC совпало) и отправить ответ что сообщение принято.

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

Тут тоже скорость обмена данными между потоками не является критичной. Не настолько чтобы заниматься расшариванеим памяти и всем вот этим вот. Поэтому была реализована система "почтовых ящиков". На винде это mailslot, в других системах - локальный именованный Unix Socket. И опять - вся синхронизация отдается системе. У каждого потока (или процесса) свой ящик, остальные, кому надо что-то передать, просто бросают туда "посылочку" - датаграмму. Получается конвейер - получил пакет, быстро провалидировал, отправил подтверждение, пакет уходит в другой поток на обработку. Обработался - уходит в третий на отправку. Практика показала что все это стабильно работает в тех объемах нагрузки, которые у нас были в реальной жизни. И при этом достаточно экономно по ресурсам (компы в диспетчерских были очень простенькие).

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

Были бы иные задачи - были бы иные подходы. Любое решение всегда идет "от задачи" и ее граничных условий. Первое что смотрим - где узкие места, требующие проработки и в первую очередь решаем именно эту проблему. Упираться и тратить 80% времени для повышение производительности в 0.1% в подавляющем большинстве случаев слишком большая роскошь (и да, бывают исключения).

А почему не сделать как, извините, в Rust и вместо манипулировании лямбдой не возвращать из функции, например, modify() обертку, которая в конструкторе будет лочить мьютекс, а в деструкторе разлочивать, и будет давать доступ к переменным внутри обернутого класса?

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

auto [data, lock] = d.modify();

но видимо код был идеален и поэтому не изменился


А зачем тут отдельно lock?

Ценю ваш сарказм :) ответил выше.

В Rust-е наоборот общие данные "живут" внутри мьютекса. Но то что вы описываете скорее можно назвать мутатором. На мой взгляд, такой поход более опасный чем лямбда т.к. если сохранить где-нибудь мутатор исходный объект останется залоченным. С лямбдами такие риски меньше. Но если нравится такой подход можно посмотреть в сторону Boost Synchronized Value или Folly Synchronized - там это реализовано, в конце статьи писал об этом.

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

У вас главное правило идеологии C++ нарушено. Object creation is object acquisition.

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

Соответсвенно нужен класс обёртка, который в дееструкторе будет вызывать его разблокировку.

Выше уже об этом писали.

Если у вас в Kaspersky Osтакая архитектура, то вызывает беспокойство ее светлое будущее.

Если бы мне пришлось снулая писать микроядерную ОС , я бы посмотрел в сторону Plan-9, и на QNX в качестве источников для вдохновения и руководства по написанию параллельного кода.

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

Здесь есть некая оптимизация на стандарт языка — если используем 17-й стандарт, нам доступны shared-мьютексы, и нам доступны shared-мьютексы, и мы можем позволить нескольким потокам обращаться к данным на чтение.

вот тут интересно! Если все(!) потоки обращаются к данным только на чтение то данные вроде как лочить не нужно,

если несколько потоков обращаются к данным на чтение, при этом хотя бы один поток МОЖЕТ в это время обратиться к данным для их изменения данные придется лочить чтобы читатели не прочитали частично измененные данные (то есть не валидные). Но если вы лочите данные перед чтением от записи вы залочите данные и от другого чтения, поэтому мне кажется эта сентенция из статьи несколько надуманной, откуда тут возьмется "некая оптимизация ", или что имеется ввиду?

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

Но если вы лочите данные перед чтением от записи вы залочите данные и от другого чтения

Нет, в шареном мьютексе вы можете читать данные из разных потоков одновременно

Мне кажется, никакого выигрыша в перфомансе от shared_мьютексов нет.

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

Если же расшаренные данные -- это пара-тройка int-ов, то да, вопрос открыт.

int не проще запихать в atomic?

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

Пример: статистика по вызовам и времени работы функции. С независимыми атомиками может случиться, что на читающей стороне вы получите условные fn_calls_under10ms == 100, fn_calls_under100ms == 10, fn_calls_long == 1, а fn_calls_total == 110 (просто потому что он инкрементируется последним). Иногда это приемлемо, а если нет - придётся или делать atomic<call_stats_struct>, или с мьютексом, с мьютексом как будто бы выглядит проще...

НЛО прилетело и опубликовало эту надпись здесь

Про мьютекс можно забыть, а с atomic помнить о мьютексе не надо.

Когда int один -- проще.
Но я говорил о нескольких int-ах.

Бывает, когда независимость этих int не имеет значения.

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

а объем захваченных данных обычно не настолько велик, чтобы потоки пересекались друг с другом на чтении этих данных

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

 шареном мьютексе вы можете читать данные из разных потоков одновременно

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

Если все(!) потоки обращаются к данным только на чтение

Не совсем понятно тогда как это работает? Есть все только читают, то вообще никаких проблем нет.

А если чтение-запись, то, мне казалось, это решается через critical sections скорее, нежели через мьютексы...

это решается через critical sections скорее, нежели через мьютексы

так это в общем то то же самое только critical sections это локальные объекты, а мутексы именованные системные, их видно из других процессов.

Ну если работаем в рамках одного процесса и нескольких нитей, то почему бы не критические секции?

Если в рамках нескольких процессов и расшареной памяти - то да, только системные объекты синхронизации (мьютексы, может быть где-то как-то семафоры)

Ждём статьи с названием "Вы все ещё пишете на С--?"

Спасибо за статью! Позвольте вопрос по теме многопоточности.
Можно ли обращаться к полям и методам объекта А из объекта Б, если они в разных нитях живут? Evgenii Legotckoi говорит что можно, но так ли это? И если всё же нельзя, то как быть?

Очевидно, смотря как написан объект.

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

Нет, не пишу.

Потому что всё ещё пишу многопоточку на де-факто Си, без ошибок синхронизации.

Синхра — это как кровоток в организме. Туда ничего постороннего в принципе не должно попадать. Никому туда нельзя руками залезать.

Сначала делается фреймворк, потом в нём отлаживается многопоточка, а потом к нему подключаются плагины, которым вообще не нужно знать, как она устроена. Когда пишешь новый плагин, нельзя «забыть поставить мьютекс» — плагины не ставят мьютексы. Плагины просто обращаются к API фреймворка. Иначе постоянно будет возникать ситуация, когда снова что-то забыли и снова грохнулась вся система.

Самое близкое практическое приближение к вышесказанному — вот. API там, правда, нет — всё уныло вкомпилировано. Но зачатки подхода вполне видны, и если рассматривать unrar.dll как единственный доступный плагин к сему поделию — то даже и не зачатки, а вполне реализация.

Вы все еще пишете многопоточку на C++ с ошибками синхронизации?
Тогда rust идет к вам!

это уже фиксанули

В этих библиотеках даже беглым взглядо видно большое количество unsafe блоков. Авторы решили где-то обхитрить Rust, но просчитались, бывает.

Ну да, просто на ровном месте решили "обхитрить", от нечего делать, видимо.

А разве нет?

"Make any value Send + Sync but only available on its original thread. Don't use on multi-threaded environments!" - что-то не похоже на "ровное место".

А разве нет?

Нет. Я этим крейтом никогда не пользовался, но из его описания и примеров я так понимаю, что он может применяться например для FFI, поскольку наличие (или отсутствие) трейтов Send и Sync помогает только непосредственно в растокоде. Внешний код, который работает с неким растовским объектом, все еще может попытаться работать с ним из разных потоков, и этот крейт пытается контролировать этот момент в рантайме. "От нечего делать" такие вещи не делают, видимо, имеется потребность.

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

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

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

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

Название класса вообще не говорит о том что это такое, лучше назвать максимально конкретно типа ReadWriteLockedState. И да, если реализация поменяется, лучше и название менять (еще лучше - создать новый класс с новой реализацией), чтобы было понятно что это и зачем без изучения кода. Названия методов тоже зачем то отличаются от того, что реально делается. Зачем выдумывать новые слова когда уже есть read и write в коде, вместо ваших view и modify? Зачем странные методы типа when? И без единого комментария о том, что он делает.

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

если это внутрення реализация компании и вы можете позволить себе использовать новую версию везде

Одно из другого не следует, если проект кросс-платформенный. У нас, например, ради поддержки легаси-платформ один и тот же код компилируется и в C++17 и в C++98.

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

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

зы. код скриншотами и #ifdef вокруг C++17 это какой-то ;№""@#

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

Mutexed<int> m;

Print( *(m.LockPtr()) );   // read access

*(m.LockPtr()) += 1;       // read-write access

// scoped access
{
   auto p = m.LockPtr();
   Print(*p);
   *p += 1;
  // ...
}

Тут я вижу ручной вызов LockPtr, это вовсе не "доступ через оператор -> автоматически порождает захват и освобождение мютекса"

ну это работает по разному

например, если внутри находится вектор, то ты можешь написать m->push_back(val) и оно будет работать именно так, как я и написал

Предусмотрены notifyOne и notifyAll. А when — метод condition variables, куда передается предикат. Он останавливает цепочку выполнения до тех пор, пока предикат не выполнится для общих данных.

И получим...

кишки реактивного программирования

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

Отсюда и все проблемы подхода.

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

Если мы меняем входную переменную все зависимые задачи нужно пересчитать. Отсюда получается простой алгоритм:

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

  2. Посчитать требуемые задачи, ставшие невалидными (задачи, которые не нужны можно и не считать).

Всё вроде замечательно и вполне будет работать.

Только реальность как всегда всё ломает. Есть целый класс задач, которые приведут к очень большой неэффективности данного подхода - назовём их "Get Part Of".

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

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

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

Моя школа скептически относится к использованию всяких "санитайзеров" и "инспекций" кода, приоритизируя грамотный дизайн кода вместо этого. Здесь же автор сначала описывает классические симптомы "г****кода", который то работает то нет, и падает неожиданно, а потом, на полном серьезе предлагает "улучшить санитайзинг и инспекции". Окей, а какие еще варианты решения предлагает автор в самом начале? Еще эти: вообще отказаться от многопоточности, фактически, и - использовать готовые библиотеки, и (!)(!) - использовать другой язык. Оуоуоу.

Но какая же самая главная ошибка в рассуждениях? Самая главная имхо ошибка, это сама предпосылка что "мютекс живет отдельно от данных, своей жизнью". Это как так-то? Это ж значит вы его сами и разместили отдельно от данных? Своими руками-то, лично? Там есть еще аргумент, что "вдруг потом кто-то классы поменяет, и мютекс заживет своей жизнью". А вы не рассматривали вариант - просто не пускать менять классы тех, кто не понимает как мютексом пользоваться?

А как им кстати пользоваться? А мютекс ВСЕГДА должен быть вместе с защищаемыми данными. И размещение мютекса отдельно, это классический ляп любого студента или джуна. В данном случае, автор видимо открыл для себя эту проблему, и придумал решение в виде описать интерфейс/абстрактный класс, требующий чтобы делать как правильно. ЭТО ХОРОШО, автор нашел правильное решение, вот только делать это можно и без изобретения класса SharedState, а просто знать как правильно, и так делать.

Я понимаю автора - сам немало велосипедов настроил когда-то, под соусом "великих методик". Это нормальный процесс обучения, и набора опытом. Поэтому, я статью плюсую.

Но в то же время, меня смущает три момента:

1) Автор всерьез предлагает как решение проблемы "усиление санитайзинга и инспекции" и прочего статического анализа кода. Че серьезно, это решение? Это значит, что окружение, коллеги автора, тоже так считают. И это печалька.

2) Просто шокирует простыня комментов под статьей. Это значит, что есть огромная аудитория в С++-мире, которая не знает как делать правильно синхронизации? Предложения спинлоков как варианта решения тоже "доставило". Или в чем дело?

3) Я пришел по статье из канала про КасперскиОС - из чего можно предположить что эту ОС пишут джуны и студенты? Вот это уже пугает.

Лучше всего делать не отдельный класс, а класс шаблон - наследник от какого - нибудь стандартного std::shared_ptr, но со встроенной синхронизацией доступа (тип которой можно задавать в зависимости от задачи), либо не использовать синхронизацию вовсе.

Пример реализации такого подхода сделан вот тут

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Информация

Сайт
www.kaspersky.ru
Дата регистрации
Дата основания
Численность
5 001–10 000 человек
Местоположение
Россия