Pull to refresh

Comments 147

PinnedPinned comments

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

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

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

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

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

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

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

Хмм..

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

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

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

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

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

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

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

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

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

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

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

композиция нескольких функциональных и коммуницирующих акторов может стать нефункциональной и некоммуницирующей.

Так зависнуть может что угодно, при чем тут акторы как таковые? Другое дело, что если сделать классическую модель - под каждый обработчик акторов - свой процесс (не под каждый актор свой процесс, хотя и такое тоже наверное можно), и отдельно к ним процесс PMON/SMON (process/service monitor), который будет проверять, жив ли тот или иной процесс-актор и не ушел ли он в сильную задумчивость, и убивать его через kill() - то тогда будет еще какой-то шанс написать приложение, в котором отдельный пользователь со своей глюкнувшей или зависшей сессией не будет убивать весь сервис целиком (см. архитектуру Oracle database или PostgreSQL).

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

Но это довольно высокая культура разработки, и что самое невероятное - готовых библиотечных решений нет (как минимум не гуглятся), книжек тоже на этот счет не написано, а в США наверное все это еще и густо-часто покрыто патентными ограничениями, других объяснений нет. Хотя архитектуре PMON/SMON уже +30 лет, если не больше.

Я не профессионал в данной области, но мне кажется, что вы описали принцип работы супервизора в эрланге https://www.erlang.org/doc/man/supervisor.html

Примерно похоже, да, но вот то что описано по ссылке выше - это и systemd и какой supervisord может делать.

Но нужно не только растартовать fail-fast-then-die процессы. Нужно еще и в таймауты уметь, а они для каждого действия могут быть разные.

И вызвающим сервисам тоже кому-то ответы слать нужно, желательно чуть более осмысленные, чем просто в виде "500 internal error". И периодические плановые рестарты (как профилактика утечек памяти и не только), и изоляция/запрет повторных вызовов, которые опять убивают или вешают акторов (DDoS).

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

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

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

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

Это все выше и выглядит как классический датабазный dead lock detection. Которые разрешают те самые SMON/PMON/Supervisors - отдельные процессы, которые постоянно мониторят workerов на предмет их нездоровья, и убивают потом виновников или просто кого попало. В т.ч. и Deadlocks так и разрешают - убивают одного из участников клинча.

Классика, в концепциях еще в 1991-м году была описана.

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

Они же и глобальную или какую там память потом туда засунули

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

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

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

Сомнительное утверждение. Ну вот вернул get-ер строку (указатель+размер), сделав при этом пару CAS-ов или какие тяжеловесные POSIX pthread_mutex_lock, а дальше что? Он вернул шаренные данные, которые уже ничем не защищены, вызывающий может их и изменить прямым доступом к памяти и отдать еще кому-то, защититься от этого не получится. Можно конечно передавать ссылки только на специально сделанные копии, типа вызывающий сам потом их чистит, но это такое, апологеты zero-copy не оценят.

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

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

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

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

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

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

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

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

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

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

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

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

На самом деле в мире существуют более продвинутые модели. К примеру есть такая база данных LMDB, и ее производная MDBX. Продвинутая Berkeley DB на стероидах.

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

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

И это пока наверное наиболее разумный механизм обеспечения согласованности и защищенности разделяемых данных в общем случае, для типовых бизнес приложений (конкуретные разряженные матрицы для симуляции ядерных реакторов в суперкластерах пока опустим), ну так, чтоб было прям реально дуракоустойчиво. В какой-то степени это реализация https://en.wikipedia.org/wiki/Readers–writer_lock , но несколько круче: ACID + защита памяти от записи и появляется понятие версионности данных.

Дальше больше. Нужно просто доделать враппер над этой MDBX, чтоб он пользовательские объекты представлял с автоматическим маппингом в этои самые key-value (не каждое поле в отдельный value, это клиника, скорее маппинг всех полей объекта в value, на и в key какой классический PK-идентификатор, вместо указателя ) и все шаренные-конкуретные данные в этой in-memory database и хранить. Хоть свой какой, хоть protocol buffers или аналоги.

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

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

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

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

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

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

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

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

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

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

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

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

Должны ли они учитывать мнение друг друга? Конечно должны.

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

Он должен дать что-то попроще, вроде инструкций - "сталь 20ХНА, значит закручиваем динамометрическим ключом, усилие - 5 ньютон-метр".

Вот так и тут. Есть прикладные программисты, которые бухгалтерии пишут, а есть те, кто серверы баз данных разрабатывают. Это разные миры. Требовать от 1С-ника уметь в барьеры и семафоры - это так себе затея. Впрочем, редко какая библиотека такое требует, мир не настолько безумен.

Другое дело что какой вчерашний 1Сник может начитаться ненужных книжек про Concurency in Action и давай себе чудить что-то эдакое на своих вебсервисах для заказа пиццы на такси.

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

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

Но высокопарные рассуждения про MDBX/SQL...

Что это? Опять у кого-то пригорело? Нужно срочно извиниться, чтоб не заминусовали карму или что?

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

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

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

Что это?

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

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

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

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

Ну там и про CoW тоже ничего не было, page faults, madvise(), offset pointers и что теперь? Общий посыл был и так понятен - мутексы и в целом примитивы синхронизации это точно не то, что нужно для прикладной (не системно-библиотечной) разработки общего назначения. Прикладник должен писать предельно простой линейный код без оглядки на конкурентные примитивы, среда (фреймворк) должна обеспечивать конкурентную корректность из коробки.

Ибо постоянно изменяющиеся бизнес-требования никак не способствуют поддержке корректной работы многотредного кода, тут никакой Америки нет, и так все всем давно известно.

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

А вот для системно-библиотечного уровня вопрос правильно организованной конкурентности и так более менее индустрией решен - берем исходники PostgreSQL, Linux Kernel или т.д. чего иного более менее успешного и стабильного, и изучаем оные подходы до полного прозрения и способности скопировать по образу подобию, ничего нового там не придумать, все было придумано еще в 70-х.

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

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

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

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

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

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

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

Kaspersky OS

Да хоть BolgenOS. Википедия говорит что там в основе ядра С, на офсайте пишут что С и С++, качать проверять это все совсем не хочется.

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

Ну конкретно по RCU -

https://en.wikipedia.org/wiki/Read-copy-update

упоминается VM/XA, и в ссылках переход на статью

http://www.rdrop.com/users/paulmck/RCU/RCUdissertation.2004.07.14e1.pdf

и там на первых листах про то, что вот раньше трава была зеленее и в целом в 1975 все об... стояло не то что сейчас.

Как по мне - это лишь одна из многих lock-free техник, производная от CAS. В основе подхода - шарить только то, что read/only, и сериализовать (до одного писателя) все, что read/write, и кто против? И это теперь всегда обязательно надо называть RCU, а не MVCC? ок, пусть будет RCU, возьмем за основу термин, широко известный в узких кругах ОС строителей, чтоб никому не было так обидно.

Больше претензий к терминологии-аббревиатурам никаких нет? А там еще TBCC есть, timestamp based concurency control, она-же insert only database, она же Log Database, она же EventSourcing, она-же ... как не обидно принято сейчас называть вот это вот все?

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

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

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

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

И к чему это было сказано? Там куча "умных" слов про одно и то-же, когда вместо INSERT/UPDATE в базу данных всегда идет только INSERT, на каждое изменение. Тоже механизм обеспечения конкурентной согласованности. В разное время назывался своими именами. А суть всегда одна - на каждый чих вставляем в базу новый object state, целиком. Можно в базу, вставлять, можно в какой application offheap.

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

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

К тому, что ваш бенефис знаний о подходах из СУБД в комментариях к данной статье является офтопиком. Может быть он был бы уместен, если бы статья пыталась сравнивать разные подходы к решению проблем конкурентности в 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-ов без mutex-ов.

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

А, впрочем, если ты ничего не знаешь про lock-free message passing, и к примеру про вот этот частный случай https://habr.com/ru/articles/130113/

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

Только не надо стенать что это про Java и вообще офтопик. Это про банальный circular lock-free buffer queue, в т.ч. multi-writer/multi-reader, оных вариаций и имплементаций вагон и тележка.

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

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

Если вы настолько уверены, что известные вам принципы работы СУБД

Иногда все-же лучше жевать, чем говорить? При чем тут принципы БД офтопик? С какой стати они вообще офтопик, если там все очень плотно на конкуретном доступе и реализовано?

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

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

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

На чем там остановились? Что мутексы это круто и они очень хорошие и они всегда нужны? Ок, эту мысль зафиксировали. Есть еще что-то сказать, так, чтоб с элементом новизны и оригинальности?
Вот про RCU мне понравилось - за столько лет опыта я ни разу про нее не слышал и даже статью в Википедии ранее не читал, и реально сам сильно удивился, почему так, прям очень давно такого не было.

Есть еще интересные-нескучные аббревиатуры?

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

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

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

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

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

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

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

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

Подумайте.

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

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

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

Нет.

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

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

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

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

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

Так многотредность к производительности обычно никакого отношения не имеет, даже наоборот, может сильно ее просадить (парадокс, но все эти барьеры памяти и атомарные операции в моменте превращают 32 ядра в одно, да еще и без L1/L2 кеша, хоть и краткосрочно).

Автоматический параллелизм и вовсе сдулся, за исключением SIMD операций, вот там да, прогресс прям сильно впечатляет, особенно на каких memcpy(), stnlen(), после ручной оптимизации-имплементации оных на ассемблере.

По всей задаче в целом.

В современном мире многотредность - это чаще всего архитектурный костыль вокруг блокирующего I/O, больше ни для чего она особо и не нужна. А вот переход от многотредно-блокирующего I/O на event based model (epoll) может в разы увеличить пропускную способность, nginx/haproxy/etc это неоднократно уже вроде доказали, в сравнении с каким apache httpd.

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

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

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

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

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

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

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

PS. Очевидно, что @SpiderEkb в своих комментариях выше говорил о parallel computing. 

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

Есть примеры эффективно распараллеливаемых задач с интенсивным конкурентным доступом к данным?

Нет, вот серьезно, личные примеры-истории успеха применения OpenMP?

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

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

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

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

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

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

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

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

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

Почему интересно - чисто как мотивация упростить базовые APIs в фреймворке, выбросив примитивы многотредовой синхронизации (начиная с pthread) вообще как понятие, ограничившись лишь CAS (а то и вовсе memory barrier) на inter-process message passing.

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

Уже несколько лет пытаюсь найти подобные задачи, ни разу не встречались (а вот эффективные MPP с тотальной изоляцией исполнителей встречались постоянно, тут и спорить нечего)

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

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

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

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

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

Все что я мог рассказать ... сделал.

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

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

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

и обращаться с вами приходится как с ребенком.

Ок, вот сейчас я честно просмотрел и статьи выборочно, и restino, и даже sobjectizer ...

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

Несмотря на то, что я разделяю, в определенной степени, ваши восторги относительно наличия 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 раз, наверное иначе не умеют даже там.

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

В смысле недоступно? Это как? А на каком уровне доступно?

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

Это факт. Там много и другой legacy и досадной наносной ерунды, вроде совместимости с Windows. Но кстати можно писать и напрямую в mmap()-ленную память, файловые блокировки и write() вызовы там просто как способ обойти ограничения mmap(), через него нельзя размер файла увеличивать. Но если замапить заранее достаточно большой sparse файл и открыть его через WRITEMAP...

Тут же нужно какой момент учитывать. LMDB/MDBX это про персистентность, на фоне стоимости fsync()/msync() и в целом записи на устройство эти расходы на файловые блокировки ничтожны. Но если в персистетность не надо - то MVCC подход можно реализовать и без файловых блокировок. Тут важна скорее идея - все мутации складываем отдельно, шарим всегда только немутирующиеся данные, а в финале мутации просто атомарно переставляем указатель на новую версию вселенной.

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

Очевидно что ничто в этом мире бесплатно не бывает. А мусор нужно чистить почти везде, это неизбежно (за исключением memory regions/obstacks/arenas). Длинные читающие транзакции - это просто рекомендация, чтоб не задерживать очистку мусора слишком долго, но если особо чистить и нечего, то можно и часами держать. Все зависит от.

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

На самом деле там все довольно просто. На старте берем flatbuffers и вперед. А так и до LINQ аналога API можно дорасти, если время-деньги на это будут. Там больше прикольно то, что можно ORM и всякие CRUD делать просто локально в памяти/на файлах, без необходимости похода по сети в SQL базу и десятикратных конвертаций из формата в формат. Просто получаем строку как указатель на кусок памяти в файле, и прямо так ее, строку эту, и используем везде.

А SQL просто сбоку всегда можно привинтить отдельно (простейщим FDW враппером каким для постгреса или просто .CSV файлы выдавать как external tables на лету), это кому надо сильно через SQL ходить данные смотреть (почти всем, впрочем).

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

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

Да, именно так. Лучший способ писать многопоточный код - это всегда писать его как однопоточный и не заниматься ерундой с этими смешными API - обертками над базовым CAS. Беря за основу Message passing, Snapshot Isolation, MVCC, т.е. вообще не думать про какие-то там примитивы синхронизации в runtime при обращении с теми-же строками данных (а в случае LMDB/MDBX это реально очень прикольно возвращать строку как указатель прям на участок памяти в файле).

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

А иначе и не получится, ибо legacy, ну и вообще, как ни крути базовый CAS никто не отменял. Другое дело что эти штуки должны быть глубоко закопаны внутри APIs, и прикладной разработчик про них знать ничего и не должен. В конце концов типовой разработчик на SQL он же не играется в эти ваши мутексы-семафоры-латчи-барьеры памяти с критическими секциями, он просто пишет себе SQL statements из соображений optimisitc locking, дальше оно как-то само под капотом. Или pessimistic locking, ок, SELECT FOR UPDATE один раз сделали и поехали.

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

Жаль только что индустрия в целом на это смотрит с прохладцей (ибо неканонично иметь MVCC прям в runtime), а на голом энтузиазме отдельных личностей там не выехать.

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

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

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

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

Как пример

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

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

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

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

А вот так чтоб прям в конкуретный доступ к общей памяти?

Какое нибудь моделирование-оптимизация производственных процессов, или расчет кинематики деформаций при ударе креш-тесте авто (или даже статических прочностых расчетов через МКЭ)?
Вот погуглил картинки на предмет Finite Element Method parallel Benchmark - там опять если и есть на что посмотреть, то опять MPP - разбиваем модель на независимые участки и решаем каждую часть условно независимо. Это понятно, но не сильно интересно.

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

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

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

Вот конкретно сейчас в работе. Есть две БД. Одна содержит в пределе до 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<call_stats_struct>

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

Про мьютекс можно забыть, а с 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) Я пришел по статье из канала про КасперскиОС - из чего можно предположить что эту ОС пишут джуны и студенты? Вот это уже пугает.

Sign up to leave a comment.