Как стать автором
Обновить

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

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

Какая-то "злобная отрыжка" из серии "писать статьи не умею, так хоть тупой комментарий оставлю" :)
Так какие же замечания удостоились вашего благосклонного одобрения? :)

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

Утверждение, о том, что мьютекс - это базовый примитив многозадачности, мягко говоря, преувеличение. Базовый примитив многозадачности - это accure/release семантика для атомиков. А уже поверх них можно колхозить (или брать готовыми) мьютексы. А можно не колхозить, а делать lockless алгоритмы, либо оптимистические блокировки.

Как в C++ всё сложно. Почему бы не возвращать из функции результат, который либо успех (и возвращаемое значение), либо ошибка (и её описание)?

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

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

Как с помощью атомиков перевести поток в состояния сна? Атомики безупречно важны, но и без мьютекса/ивента/критической секции тоже не обойтись.

Мьютекс реализуется через атомики. Тред спит через std::thread::yield_now или как он там в С++ называется, и это часть реализации мьютекса.

Ну уж нет, yield_now — это ни разу не сон, а максимум дремота.

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

acquire/release, lock/unlock все это суть одно и то же, захват и освобождение ресурса. Гарантия эксклюзивного доступа в конкретный момент времени. Называют по разному мьютекс, критическая секция и т.д. Я чтобы не путаться выбрал мьютекс, как наиболее распространенный. И да, это базовая сущность :)

А в С++ '?' не завезли? В Rust, если err-часть сигнатуры совпадает, то можно просто сказать foo()?, и это будет означать, дай результат Ok из foo, а если там err, то return его.

Походу "не завезли" :) но думаю макросом можно решить вопрос :)

ты реально не понимаешь что тебе всё равно придётся проверить ошибка там или нет?

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

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

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

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

Ссылка описывает модель памяти С++. Я не плюсовик, а учу раст, но там постоянно на этот документ ссылаются.

И эта штука куда более фундаментальна, чем просто "стандартная библиотека". Семантика доступа к атомикам (relaxed/accure/relese/sequential consitency) - это то, что различает программиста на С++ от ванильного мира уютных песочниц, типа python или go.

// Мне странно видеть пост от человека, который, вроде бы, знает С++ (и пишет статьи про него), который про memory ordering не знает.

А в каком контексте там на этот документ ссылаются? Может это вообще не имеет никакого отношения к обсуждаемому здесь вопросу?

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

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

В контексте "У Rust'а нет своей модели памяти, используется модель памяти С++". И это не "ядро системы" (что бы вы под этим не подразумевали), это memory model, или, ordering model.

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

А так ли этот синтаксический сахар нужен? В C++ ничего не мешает использовать алгебраические типы данных ошибок и без него (Boost.Outcome, реализации планируемого std::expected или std::variant если уж прямо совсем лень что-то подключать).

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

Без сахара стек сам себя не раскрутит и надо после каждого вызова ручками городить if (result.error) return result;, что ничем не отличается от if (error_code != 0) goto cleanup; в C.


Планируемый std::expected мог бы немного улучшить ситуацию, если бы его предлагали в авторском дизайне, но увы:


Using the indirection operator for an object that does not contain a value is undefined behavior. This behavior offers maximum runtime performance.

Без сахара стек сам себя не раскрутит и надо после каждого вызова ручками городить if (result.error) return result;, что ничем не отличается от if (error_code != 0) goto cleanup; в C.

Справедливости ради, это работает только в самом простом случае, когда типы Result совпадают у функции и у тех, кого она вызывает. Для конвертации условного IOResult в MyResult синтаксического сахара нет и это всё также надо делать ручками.

Планируемый std::expected мог бы немного улучшить ситуацию, если бы его предлагали в авторском дизайне, но увы:

Using the indirection operator for an object that does not contain a value is undefined behavior. This behavior offers maximum runtime performance.

Это стандартный дизайн типов в стандартной библиотеке C++. Например, std::optional тоже имеет UB в операторе разыменовывания, если значение не было ранее сохранено, но есть более безопасный .value(), который кидает в этом случае исключение (считай аналог unwrap() в Rust). Уверен, что и с std::expected в конце концов сделают также.

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

В этом и проблема — авторы пропозала не понимают его сути и натягивают сову на глобус, подавая expected как улучшенный optional.


Вся суть expected была в unify local and centralized error handling (именно на этот момент ссылается и пропозал, что иронично): нужна локальная обработка ошибки — проверяй и обрабатывай, не нужна — просто используй. Eсли там было исключение, то оно взлетит и будет обработано выше по стеку там, где его ждут.


В том, что предлагается к стандартизации, централизованная обработка безнадежно сломана: разыменование — это UB, а .value() — это не throw E, это throw bad_expected_access<E>, которого выше никто не ждет. Но зато похоже на optional, да.

Справедливости ради, это работает только в самом простом случае, когда типы Result совпадают у функции и у тех, кого она вызывает. Для конвертации условного IOResult в MyResult синтаксического сахара нет и это всё также надо делать ручками.

Где именно его нет? В том же Rust достаточно объявить MyResult реализующим типаж From<IOResult> чтобы подобный сахар появился.

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

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

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

Невелико отличие от проверяемых исключений. И синтаксис последних кажется мне несколько более удобным. Код в rust бывает прямо таки обвешан "?". В языках с исключениями "?" как бы есть по умолчанию везде.

Что касается проверки состояния объекта, то его нужно проводить непосредственно перед началом работы с этим состоянием, которое должно отвечать определенным ожиданиям. Например, вызван некий метод, который работает с приватным списком и ожидает, что он не пуст – иначе запускать алгоритм не имеет смысла. Если этот список пуст значит, вероятно, не был вызван другой метод, который заполняет его данными. 

Такую логику обработки, кажется, можно непосредственно встроить в некоторый класс, который описывает типичное/типовое поведение определённых объектов.

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

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

Совершенно согласен. Я даже описал такой компромисс для многострадальной (уже 103 отвергнутых предложений по улучшению) обработки ошибок в Gоlang. Конечно, моя идея тоже была отвергнута как радикальная (too drastic), как дженерики еще несколько лет назад. Смысл в том, что ошибки не должны передаваться через возвращаемое значение функции вместо нормального значения или вместе с ним, с последующей упаковкой и передачей вверх по стеку вызовов (error wrapping), а должны становиться доступными мгновенно и везде через специальный канал сообщений и через подробный лог (и то, и другое, параллельно).

Краткое описание:

  1. Когда функция вызывается, указатель на ее экземпляр (instance) сохраняется в указанной программистом переменной для будущего доступа.

  2. Когда генерится ошибка, ее тип связывается с указателем на экземпляр функции.

  3. Для проверки наличия ошибки необходимо указать указатель на экземпляр функции, а также тип ошибки или метаданные ошибки из лога.

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

Это получается что-то вроде асинхронного вызова? Типа фьючерса где можно узнать результат?

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

Насколько удобно писать в таком стиле?

"Это получается что-то вроде асинхронного вызова?"

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

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

Это предложение о существенном изменении языка Golang. Имеющимися средствами без изменений компилятора и рантайма это реализовать, наверное, невозможно. Хотя для других языков - кто знает?

"Насколько удобно писать в таком стиле?"

Синтаксис минималистичен, никаких навороченных абстракций. Гораздо удобнее, чем то, что сейчас есть в Golang, а главное, появляются возможности легкого управления горутинами, что сейчас достигается большим количеством программного кода (каналы, пакет context). Посмотрите сами: https://github.com/golang/go/issues/50280

Например:

Operation& Function(args...)
{
  rezult=...
	return new FunctionOperation(args,rezult);
}
...
op=Function(Source,Destianation);
...
if (op.IsCorrect()) { ... } 
...

Если у вас сразу после создания объект инвалид, то есть смысл бросать исключение в конструкторе.

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

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

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

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

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

только почему свой в кавычках?

Потому что это переизобретение велосипеда.

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

Еще по поводу статьи. Если вы сделали упор на C++, то слишком много отсылок к другим языкам, а вот эта рекомендация:

Выстраивайте цепочки задач, если системная библиотека поддерживает подобные изыски. Например, C#:

К C++ вообще 1-в-1 не применима, т.к. в C++ "системная библиотека" не поддерживает подобные изыски. Ссылка на C# выглядит как троллинг.

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

Потому что это переизобретение велосипеда.

Пожалуйста ссылку на класс из какой-нибудь системной библиотеки, который реализует схожую функциональность с SharedState.

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

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

Если вы сделали упор на C++, то слишком много отсылок к другим языкам

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

Ссылка на C# выглядит как троллинг.

Да нет, и в мыслях не было троллить :) задачи есть в C++, а реализовать continueWith и waitAll можно и самому. Это просто был пример удобного API, который в C# уже есть "из коробки".

Как и непонятно будет как task-и сочетать с вашей серебряной пулей

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

Пожалуйста ссылку на класс из какой-нибудь системной библиотеки

Зачем?

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

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

И этого, прямо скажем, недостаточно.

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

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

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

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

Зачем?

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

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

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

И этого, прямо скажем, недостаточно.

Хорошо, принимается. Однако для меня не очевидно чего именно не достаточно. Не могли бы вы кратко написать что по вашему мнению необходимо добавить?

Вы пытались написать как упростить работу с многопоточностью вообще, но с примерами на C++?

Да.

При использовании task based пересечение по разделяемым данным должно быть минимальным

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

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

Если все таски будут выполняться последовательно друг за другом, то где здесь многозадачность? ;)

Затем, что вы голосовно утверждаете будто бы я изобрел "велосипед"

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

Прямых аналогов сейчас указать не могу. Но посмотрите, например, на Mutex из Rust-а: там объединение mutex-а и защищаемого им объекта T происходит на уровне типа Mutex.

Еще ваш подход заставляет вспомнить RCU -- Read-Copy-Update (хотя им и не является).

Однако для меня не очевидно чего именно не достаточно.

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

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

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

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

Отдельный вопрос -- это как быть с вашим подходом в случае, если требуется синхронно модифицировать несколько разных StaredState объектов.

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

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

Если все таски будут выполняться последовательно друг за другом, то где здесь многозадачность? ;)

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

Но посмотрите, например, на Mutex из Rust-а: там объединение mutex-а и защищаемого им объекта T происходит на уровне типа Mutex.

Спасибо, хороший пример. Действительно здравая идея объединить mutex и защищаемые данные. Только еще condition variable не прикрутили :)

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

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

Отдельный вопрос -- это как быть с вашим подходом в случае, если
требуется синхронно модифицировать несколько разных StaredState
объектов.

Вложенные SharedState.

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

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

Кто это сказал?

Я, но это была шутка :) ваша мысль мне понятна.

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

Как я понимаю, ваш SharedState дает пользователю две полезные вещи:

  1. Объединение защищаемых данных и объекта синхронизации "под одной крышей". Что защищает от того, что какую-то часть данных мы модифицируем не захватив объект синхронизации.

  2. Упрощается использование разделяемых данных в режиме read-calculate-modify.

Причем в своей статье вы акцентируете штуку номер 2. Но если вы ее акцентируете, то нужно бы и сказать, что за этим последует. А если не обращать на нее внимания, используя SharedState как простой mutex, то значит этой полезной штуки как бы и нет.

Вложенные SharedState.

Не понятно. Вы что-то такое подразумеваете:

struct DataA {
  ...;
};
struct DataB {
  ...
  SharedState<DataA> a_;
};
SharedState<DataB> b;

И работать с DataB::a_ можно только когда захвачен b?

Как я понимаю, ваш SharedState дает пользователю две полезные вещи

Еще можно дожидаться определенного состояния данных и нотифицировать об этом.

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

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

Это скорее концептуальные преимущества, а не технические.

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

Не понятно. Вы что-то такое подразумеваете

Да, только внутри DataB нужно, наверное, поместить хотя бы еще один SharedState иначе непонятно зачем такой огород городить :)
Может вы объясните какой юзкейз мы пытаемся покрыть в данном случае? Тогда что-нибудь поизящнее попробуем выдумать :)

Еще можно дожидаться определенного состояния данных и нотифицировать об этом.

Поскольку вы спрятали объект синхронизации от пользователя, то вам приходится брать на себя задачу предоставления чего-то вроде event/condition_variable. Так что это не столько достоинство вашего подхода, сколько последствия его особенностей.

Кстати говоря, ваш SharedState всегда сопровождается condition_variable (event)?

Помимо этого мы не "лочим" данные

Ну да, ну да. Это не лок. Выглядит как лок, работает как лок, но не лок.

Может вы объясните какой юзкейз мы пытаемся покрыть в данном случае?

Одной синхронной операцией модифицировать сразу несколько независимых друг от друга SharedState. Как раз то, для чего в C++ную stdlib добавили std::lock.

Так что это не столько достоинство вашего подхода, сколько последствия его особенностей.

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

Вон в Java прямо в базовый Object добавили wait/notify/notifyAll :)

Кстати говоря, ваш SharedState всегда сопровождается condition_variable (event)?

Да.

Ну да, ну да. Это не лок. Выглядит как лок, работает как лок, но не лок.

Хорошо давайте посмотрим на это так - вы смотрите код и видите "залочить мютекс". Какие данные он защищает? Когда его можно разлочить?

Или вы видите в коде "читаю данные". И смотрите на небольшой блок кода в котором они читаются.

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

Одной синхронной операцией модифицировать сразу несколько независимых друг от друга SharedState.

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

Может нужен просто еще один SharedState в котором будут собираться данные из остальных?

Либо вариант, который вы уже предложили.

Либо внутри блока доступа к одному SharedState открыть остальные на чтение или запись.

В моем понимании это все-такие достоинство

Странно было бы, если бы вы думали иначе.

Или вы видите в коде "читаю данные". 

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

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

мне непонятно зачем может понадобиться одновременно лочить независимые SharedState? 

Ну раз непонятно, значит ваш SharedState для таких сценариев не подойдет.

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

Если кэши защищаются обычными мутексами, то мы можем это сделать через std::lock.

Как это сделать через ваши SharedState -- непонятно.

Выглядит как какой-то костыль при кривом дизайне системы.

Если вы чего-то не понимаете или с чем-то не сталкивались, то это не значит, что дизайн кривой.

У меня есть устойчивое подозрение, что вы просто не осознаете, зачем нужен std::lock и почему он был добавлен.

Либо вариант, который вы уже предложили.

Я ничего не предлагал. Просто пытаюсь выяснить что следовало бы сделать в вашем подходе.

Странно было бы, если бы вы думали иначе.

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

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

Вы видите потому что я вам сказал что они лочатся :)

Я то как раз предлагал перестать мыслить в рамках лочить-разлочить.

Но, конечно, ни в коем случае никому не навязываю свое видение :)

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

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

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

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

А то, что вы на эту суть еще несколько уровней концептуальных смыслов пытаетесь навесить -- это ваши личные проблемы.

Ну не нужно горячиться :) Какие личные проблемы? Вы о чем вообще? Вы что психоаналитик? :)

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

Ну раз непонятно, значит ваш SharedState для таких сценариев не подойдет.

Зачем же так категорично? :) Может и подойдет.

Если вы чего-то не понимаете или с чем-то не сталкивались, то это не значит, что дизайн кривой.

Да вы успокойтесь, мы же не какой-то конкретный дизайн обсуждаем.

У меня есть устойчивое подозрение, что вы просто не осознаете, зачем нужен std::lock и почему он был добавлен.

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

Я ничего не предлагал. Просто пытаюсь выяснить что следовало бы сделать в вашем подходе.

Ну вы же привели кусок кода? Так вот вы все правильно поняли это и есть одно из решений.

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

Вы это сказали еще в своей статье.

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

Ахринеть, дайте два.

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

Именно это и должен понимать потенциальный пользователь выбирая подобный подход.

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

Да вы успокойтесь, мы же не какой-то конкретный дизайн обсуждаем.

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

Ну вы же привели кусок кода?

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

Так вот вы все правильно поняли это и есть одно из решений.

Это не решение. И мне кажется, что от слова совсем.

Ахринеть, дайте два.

Увольте пожалуйста от такой "камасутры" :)

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

Именно это и должен понимать потенциальный пользователь выбирая подобный подход.

Совершенно верно.

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

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

Выбор за разработчиком.

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

Во-первых, нас никто не вынуждает.

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

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

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

Это не решение. И мне кажется, что от слова совсем.

А... ну раз кажется тогда, конечно :)

> Кстати говоря, ваш SharedState всегда сопровождается condition_variable (event)?

Да.

То, что у вас всегда внутри SharedState живет condition_variable, -- это еще один недостаток вашего подхода. Т.к. нарушается принцип не платить за то, что не используется.

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

То, что у вас всегда внутри SharedState живет condition_variable, -- это
еще один недостаток вашего подхода. Т.к. нарушается принцип не платить
за то, что не используется.

А почему вы заранее решили что это не будет использоваться? Да еще сразу же в недостатки записали :)

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

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

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

Термин "Критическая секция" это, если не ошибаюсь, изобретение Microsoft. По своей сути это тот же мьютекс. Что касается имплементации то название "мьютекс" (mutually exclusive access to shared data structures in a multi-threaded environment, одно из определений) совершенно не подразумевает вызовы ядра, его спокойно можно реализовать в user space.

Например, в JVM и CLI используется еще один термин - Monitor, реализовываться может через тот же "thin lock".

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

Я бы не стал утверждать что это "обычно", это разве что в Windows так :)

Утонение: Критическая секция — это ничуть не изобретение Microsoft, а общепринятый термин для части алгоритма, не предусматривающей многопоточное исполнение. "Изобретением" Microsoft является использование этого термина как названия примитива синхронизации.


Кстати, Monitor тоже изобрели не в JVM и не в CLI. И он тоже используется в переносном значении, поскольку изначально был языковым механизмом, а не библиотечным примитивом.

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

За то, что решения подаются, как серебрянная пуля без недостатков/ограничений области применимости, что, очевидно, не так. Да и сами по себе решения не являются изобретениям (подобное и ранее встречал, и реализовывал в том или ином виде - тоже).

Ну и что? Встречал, реализовывал но не описывал ведь?

Человек описал, дал посмотреть на код. В чем проблема то?

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

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

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

Публикации

Истории