Комментарии 27
Спасибо за статью!
Пару вопросов:
1) А почему бы не использовать recursive_mutex? Или лучше задать тип мьютекса дополнительным параметром шаблона. Тогда можно и разрешить копировать Accessor, и множественно вызывать SharedResource::lock().
2) Поддержка const SharedResource намеренно отсутствует? Можно реализовать, сделав m_mutex mutable и добавив класс ConstAccessor, который будет возвращаться из SharedResource::lock() const.
Пару вопросов:
1) А почему бы не использовать recursive_mutex? Или лучше задать тип мьютекса дополнительным параметром шаблона. Тогда можно и разрешить копировать Accessor, и множественно вызывать SharedResource::lock().
2) Поддержка const SharedResource намеренно отсутствует? Можно реализовать, сделав m_mutex mutable и добавив класс ConstAccessor, который будет возвращаться из SharedResource::lock() const.
+2
По-хорошему mutex и должен быть mutable, потому что он internally-synchronized и в навязывании константности не нуждается.
0
Спасибо за вопросы
1) Честно говоря не обдумывал такой сценарий. Возможно это действительно имеет смысл. Не подскажете, в каких случаях это могло бы быть полезно, чтобы подумать, как лучше реализовать?
2) Я думал об этом, но тогда, по-хорошему, нужно использовать не мьютекс а read-write lock и соответствующим образом лочить его при получении Accessor'а. Но в стандартной библиотеке их нет, а вносить лишнии зависимости, усложняя статью не хотелось. Оставить просто mutex и сделать ConstAccessor просто для поддержки константности можно и даже нужно. Спасибо за подсказку, честно говоря, просто забыл об этом.
1) Честно говоря не обдумывал такой сценарий. Возможно это действительно имеет смысл. Не подскажете, в каких случаях это могло бы быть полезно, чтобы подумать, как лучше реализовать?
2) Я думал об этом, но тогда, по-хорошему, нужно использовать не мьютекс а read-write lock и соответствующим образом лочить его при получении Accessor'а. Но в стандартной библиотеке их нет, а вносить лишнии зависимости, усложняя статью не хотелось. Оставить просто mutex и сделать ConstAccessor просто для поддержки константности можно и даже нужно. Спасибо за подсказку, честно говоря, просто забыл об этом.
+1
>Это не очень хорошо, но не смертельно — перемещённые объекты и не предназначены для дальнейшего использования.
Про стандартную библиотеку сказано так:
ISO CPP 17.6.5.1: Objects of types defined in the C++ standard library may be moved from (12.8).… Unless otherwise specified, such moved-from objects shall be placed in a valid but unspecified state.
Стоит придерживаться этого и в user-defined типах.
Про стандартную библиотеку сказано так:
ISO CPP 17.6.5.1: Objects of types defined in the C++ standard library may be moved from (12.8).… Unless otherwise specified, such moved-from objects shall be placed in a valid but unspecified state.
Стоит придерживаться этого и в user-defined типах.
+1
Согласен. Но лично я не смог придумать, как можно переместить данный класс без того, чтобы его больше нельзя было использовать. Поэтому и пришлось добавить метод проверки и этим, как бы, признать подобное состояние валидным, просто не пригодным к дальнейшему использованию. Так же устроены многие move-конструкторы для многих классов стандартной библиотеки, например std::thread.
+1
Перемещаем из std::vector. Получаем в итоге пустой вектор. Пользуемся им как новеньким. Всё работает. Другой вопрос, часто ли так делают на практике? Думаю, что нет.
0
Да, это легко сделать с вектором или любым другим контейнером, или классом, для которого пустое состояние ожидаемо и часто встречается на практике. Но что делать с классами типа std::thread или, например, std::future при перемещении? Заметьте, это классы стандартной библиотеки. У std::future в описании прямым текстом написано
Так что можно считать, что предложенный мною подход вполне соответствует практике.
move constructorИсточник
The constructed object acquires the shared state of x (if any).
x is left with no shared state (it is no longer valid).
Так что можно считать, что предложенный мною подход вполне соответствует практике.
+1
Они кидают исключение если попробовать использовать такой объект, это нормально. А как минимум std::thread ещё и сконструировать в такое состояние можно, конструктором по-умолчанию. Строго говоря состояние не является invalid, поскольку оно обрабатывается объектом. Вот если бы использование около объекта вело к undefined behaviour, можно было бы сказать иначе.
К тому же после перемещения из объектов таких типов в них можно переместить другой объект, например временный, и они снова заживут :-)
К тому же после перемещения из объектов таких типов в них можно переместить другой объект, например временный, и они снова заживут :-)
0
Всё это справедливо и для моего Accessor'а кроме выбрасывания исключений. Я обдумал вариант с исключениями и решил не выкидывать исключения по нескольким причинам:
1) Лишняя проверка при простых операциях типа разыменования, которые должны быть максимально быстрыми. Конечно, ущерб от этого можно свести к минимуму с помощью likely/unlikely, но это лишние проблемы с портируемостью, так как это компиляторные атрибуты.
2) Исключения в С++ не всеми используются и не всегда включены, что тоже требует дополнительной обработки.
3) Результатом разыменования nullptr на практике почти всегда будет падение, хоть это и UB формально.
4) Падение — это хорошо в данном случае. Такие исключения, ИМХО, обрабатывать не стоит в любом случае. Однако я согласен, что остановка программы с исключением — гораздо правильнее и удобнее чем без него.
Одним словом, поддержку исключений можно сделать, и даже нужно. Но это не элементарная задача, в ней есть свои нюансы, поэтому я решил, что вынесение подобных вещей в статью только усложнит её для понимания и отвлечёт от сути. В репозитории я попробую реализовать нормальную поддержку исключений.
1) Лишняя проверка при простых операциях типа разыменования, которые должны быть максимально быстрыми. Конечно, ущерб от этого можно свести к минимуму с помощью likely/unlikely, но это лишние проблемы с портируемостью, так как это компиляторные атрибуты.
2) Исключения в С++ не всеми используются и не всегда включены, что тоже требует дополнительной обработки.
3) Результатом разыменования nullptr на практике почти всегда будет падение, хоть это и UB формально.
4) Падение — это хорошо в данном случае. Такие исключения, ИМХО, обрабатывать не стоит в любом случае. Однако я согласен, что остановка программы с исключением — гораздо правильнее и удобнее чем без него.
Одним словом, поддержку исключений можно сделать, и даже нужно. Но это не элементарная задача, в ней есть свои нюансы, поэтому я решил, что вынесение подобных вещей в статью только усложнит её для понимания и отвлечёт от сути. В репозитории я попробую реализовать нормальную поддержку исключений.
0
1) Согласен, производительность и желание zero-cost абстракции оправдывает подход. А в msvc++ реализовать likely/unlikely никакими атрибутами нельзя?
2) C++ standard library по-моему не особенно волнуется о тех, кто например вызывает operator new(size_t) при флажке -fno-exceptions (строго говоря вызывать следует operator new(size_t, std::nothrow), или вызывает какой-нибудь std::vector::at для out-of-bound индексов.
3) Не имеет отношения к теме, но интересный момент из стандарта: само по себе разыменование nullptr не является неопределенным поведением, а лишь использование результата такой индирекции :)
2) C++ standard library по-моему не особенно волнуется о тех, кто например вызывает operator new(size_t) при флажке -fno-exceptions (строго говоря вызывать следует operator new(size_t, std::nothrow), или вызывает какой-нибудь std::vector::at для out-of-bound индексов.
3) Не имеет отношения к теме, но интересный момент из стандарта: само по себе разыменование nullptr не является неопределенным поведением, а лишь использование результата такой индирекции :)
0
1) Если и можно, то я о таких способах не знаю.
2) О том и речь, что в стандартной библиотеке реализована проверка на поддержку исключений и в большинстве случаев при отключенных исключениях вызывается abort() вместо выброса исключения. Проблема в том, что в разных компиляторах эта проверка, опять же, реализуется по разному. А значит опять лишние проблемы с тестированием и поддержкой компиляторо-зависимого кода.
2) О том и речь, что в стандартной библиотеке реализована проверка на поддержку исключений и в большинстве случаев при отключенных исключениях вызывается abort() вместо выброса исключения. Проблема в том, что в разных компиляторах эта проверка, опять же, реализуется по разному. А значит опять лишние проблемы с тестированием и поддержкой компиляторо-зависимого кода.
0
Скорее, «мьютексы в стиле RAII». Я думаю, до C++11 они не использовались, как абстракция, именно из-за отстуствия variadic templates, что вызывает некоторые сложности с созданием подобных разделяемых объектов на стеке; поэтому все кушали кактус, держали мьютекс и объект отдельно, и плодили
MutexLocker lock(mutex);
в скоупах.+1
Как-то не стали заморачиваться с этим, а сделали resource_pool.
resource_pool<connection> connections({conn1, conn2, conn3});
//…
{
resource_guard<connection> conn = connections.acquire();
conn->foo();
conn->bar();
// при выходе из области видимости ресурс вернется в пул.
}
Ваш случай эмулируется одним элементом в пуле.
resource_pool<connection> connections({conn1, conn2, conn3});
//…
{
resource_guard<connection> conn = connections.acquire();
conn->foo();
conn->bar();
// при выходе из области видимости ресурс вернется в пул.
}
Ваш случай эмулируется одним элементом в пуле.
+1
А толку-то? В защищенном блоке можно сделать алиас на ресурс, даже не зная об этом. Без borrow-checker-а бесполезно.
+4
Это правда, что поделать, C++ всё-таки не Rust. Это просто попытка сделать свой C++ код хотя бы немного безопаснее в ожидании релиза Rust'а. Именно поэтому и есть раздельчик про то, чего делать с этим классом нельзя. Было бы классно, если бы был способ переложить хотя бы часть этих проверок на компилятор, но нам, плюсовикам, приходиться контроллировать такие вещи самим, к сожалению.
0
По-моему, вы заново изобрели умные мьютексы, которые были описаны в моей статье: Полезные идиомы многопоточности С++. Только у меня еще круче: можно также реализовать RW locks через длинную стрелку --->. Еще предлагаю посмотреть видео видео со встречи C++ User Group в Санкт-Петербурге.
+5
Похоже, вы правы. Хотя некоторые отличия всё-таки есть, но суть одна, согласен. Признаюсь, я с самого начала не надеялся, что изобрёл что-то принципиально новое. Целью было скорее обратить внимание на неплохую, ИМХО, идиому тех, кто раньше про неё не слышал. Ну и ещё я надеялся, что кому-нибудь из новичков, возможно, будет полезно почитать про пошаговый процесс разработки класса с желаемой функциональностью, где более-менее понятно описано, почему при разработки используются те или иные решения. Видео обязательно посмотрю, как доберусь домой, спасибо.
0
Ничего подобного. Здесь подход более правильный, lock явный. И никаких длинных стрелок!
0
У моего подхода больше возможностей: можно как явно, так и неявно. А любую из этих возможностей при желании можно отрубить. Так что идеологически — более полноценный подход.
0
Больше возможностей — не значит лучше
Простое лучше сложного
Явное лучше неявного
Более детально я в той теме уже описывал свои соображения. Вот вам еще одна ошибка, которая невозможна при явном локе и вероятна при неявном:
Простое лучше сложного
Явное лучше неявного
Более детально я в той теме уже описывал свои соображения. Вот вам еще одна ошибка, которая невозможна при явном локе и вероятна при неявном:
struct c {int x};
AnLock<c> v;
v->x = foo(v->x); // deadlock or not atomic depending on compiler
0
Вот вам еще одна ошибка, которая невозможна при явном локеТ.е. так нельзя написать:
v.lock()->x = foo(f.lock()->x)
?А вообще, когда я писал про возможности, то имел в виду, что разработчик может оставить в своем проекте только те возможности, которые ему нужны. И в данном случае я считаю, что иметь выбор лучше его отсутствия. Более того, если посмотреть мою презентацию, то там я рассказываю про то, как можно избегать дедлоков при работе сразу с несколькими мьютексами, а здесь вообще про это ничего не сказано. Далее: при работе с разделяемым объектом не синглтоном возникает вопрос о времени жизни со всеми вытикающими. Как такие объекты класть в контейнер? Как изменять? Как работать сразу с несколькими объектами из разных контейнеров? Тут не очень понятно.
0
Для того, чтобы написать
Иметь выбор не всегда хорошая идея. Мне больше нравится Zen of Python
У этой статьи на мой взгляд вполне внятные рамки — предложить реализацию конкретного шаблона. И с этой задачей статья отлично справляется. Не нужно сюда приплетать разделяемые объекты.
v.lock()->x = foo(f.lock()->x)
надо быть не очень хорошим программистом, а для того, чтобы v->x = foo(v->x)
всего лишь не очень внимательным.Иметь выбор не всегда хорошая идея. Мне больше нравится Zen of Python
There should be one-- and preferably only one --obvious way to do it
У этой статьи на мой взгляд вполне внятные рамки — предложить реализацию конкретного шаблона. И с этой задачей статья отлично справляется. Не нужно сюда приплетать разделяемые объекты.
0
Здесь стоит подумать о universal references + std::forward чтобы передавать аргументы в конструктор «в первозданном виде»
=>
template<typename ...Args> SharedResource(Args ...args) : m_resource(args...) { }
=>
template<typename ...Args> SharedResource(Args&&... args) : m_resource(std::forward<Args>(args)...) { }
+6
Зарегистрируйтесь на Хабре, чтобы оставить комментарий
Мьютексы в стиле Rust для C++