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

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

Boost.Atomic совсем из другой оперы же и подходит только для POD-типов. Плюс атомики не позволяют выполнять подряд несколько операций, за исключением ограниченного списка (а-ля compare&swap и ему подобные).
Спасибо за статью!

Пару вопросов:
1) А почему бы не использовать recursive_mutex? Или лучше задать тип мьютекса дополнительным параметром шаблона. Тогда можно и разрешить копировать Accessor, и множественно вызывать SharedResource::lock().
2) Поддержка const SharedResource намеренно отсутствует? Можно реализовать, сделав m_mutex mutable и добавив класс ConstAccessor, который будет возвращаться из SharedResource::lock() const.
По-хорошему mutex и должен быть mutable, потому что он internally-synchronized и в навязывании константности не нуждается.
Спасибо за вопросы
1) Честно говоря не обдумывал такой сценарий. Возможно это действительно имеет смысл. Не подскажете, в каких случаях это могло бы быть полезно, чтобы подумать, как лучше реализовать?
2) Я думал об этом, но тогда, по-хорошему, нужно использовать не мьютекс а read-write lock и соответствующим образом лочить его при получении Accessor'а. Но в стандартной библиотеке их нет, а вносить лишнии зависимости, усложняя статью не хотелось. Оставить просто mutex и сделать ConstAccessor просто для поддержки константности можно и даже нужно. Спасибо за подсказку, честно говоря, просто забыл об этом.
>Это не очень хорошо, но не смертельно — перемещённые объекты и не предназначены для дальнейшего использования.

Про стандартную библиотеку сказано так:
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 типах.
Согласен. Но лично я не смог придумать, как можно переместить данный класс без того, чтобы его больше нельзя было использовать. Поэтому и пришлось добавить метод проверки и этим, как бы, признать подобное состояние валидным, просто не пригодным к дальнейшему использованию. Так же устроены многие move-конструкторы для многих классов стандартной библиотеки, например std::thread.
Перемещаем из std::vector. Получаем в итоге пустой вектор. Пользуемся им как новеньким. Всё работает. Другой вопрос, часто ли так делают на практике? Думаю, что нет.
Да, это легко сделать с вектором или любым другим контейнером, или классом, для которого пустое состояние ожидаемо и часто встречается на практике. Но что делать с классами типа 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).
Источник
Так что можно считать, что предложенный мною подход вполне соответствует практике.
Они кидают исключение если попробовать использовать такой объект, это нормально. А как минимум std::thread ещё и сконструировать в такое состояние можно, конструктором по-умолчанию. Строго говоря состояние не является invalid, поскольку оно обрабатывается объектом. Вот если бы использование около объекта вело к undefined behaviour, можно было бы сказать иначе.
К тому же после перемещения из объектов таких типов в них можно переместить другой объект, например временный, и они снова заживут :-)
Всё это справедливо и для моего Accessor'а кроме выбрасывания исключений. Я обдумал вариант с исключениями и решил не выкидывать исключения по нескольким причинам:
1) Лишняя проверка при простых операциях типа разыменования, которые должны быть максимально быстрыми. Конечно, ущерб от этого можно свести к минимуму с помощью likely/unlikely, но это лишние проблемы с портируемостью, так как это компиляторные атрибуты.
2) Исключения в С++ не всеми используются и не всегда включены, что тоже требует дополнительной обработки.
3) Результатом разыменования nullptr на практике почти всегда будет падение, хоть это и UB формально.
4) Падение — это хорошо в данном случае. Такие исключения, ИМХО, обрабатывать не стоит в любом случае. Однако я согласен, что остановка программы с исключением — гораздо правильнее и удобнее чем без него.

Одним словом, поддержку исключений можно сделать, и даже нужно. Но это не элементарная задача, в ней есть свои нюансы, поэтому я решил, что вынесение подобных вещей в статью только усложнит её для понимания и отвлечёт от сути. В репозитории я попробую реализовать нормальную поддержку исключений.
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 не является неопределенным поведением, а лишь использование результата такой индирекции :)
1) Если и можно, то я о таких способах не знаю.
2) О том и речь, что в стандартной библиотеке реализована проверка на поддержку исключений и в большинстве случаев при отключенных исключениях вызывается abort() вместо выброса исключения. Проблема в том, что в разных компиляторах эта проверка, опять же, реализуется по разному. А значит опять лишние проблемы с тестированием и поддержкой компиляторо-зависимого кода.
Скорее, «мьютексы в стиле RAII». Я думаю, до C++11 они не использовались, как абстракция, именно из-за отстуствия variadic templates, что вызывает некоторые сложности с созданием подобных разделяемых объектов на стеке; поэтому все кушали кактус, держали мьютекс и объект отдельно, и плодили MutexLocker lock(mutex); в скоупах.
Как-то не стали заморачиваться с этим, а сделали resource_pool.

resource_pool<connection> connections({conn1, conn2, conn3});
//…
{
resource_guard<connection> conn = connections.acquire();
conn->foo();
conn->bar();
// при выходе из области видимости ресурс вернется в пул.
}

Ваш случай эмулируется одним элементом в пуле.
На самом деле не совсем. Речь идёт о постоянном объекте (ресурсе), который защищён мьютексом, но не освобождается в случае выхода из блока кода. Но за подход спасибо, взял на заметку )
А толку-то? В защищенном блоке можно сделать алиас на ресурс, даже не зная об этом. Без borrow-checker-а бесполезно.
Это правда, что поделать, C++ всё-таки не Rust. Это просто попытка сделать свой C++ код хотя бы немного безопаснее в ожидании релиза Rust'а. Именно поэтому и есть раздельчик про то, чего делать с этим классом нельзя. Было бы классно, если бы был способ переложить хотя бы часть этих проверок на компилятор, но нам, плюсовикам, приходиться контроллировать такие вещи самим, к сожалению.
По-моему, вы заново изобрели умные мьютексы, которые были описаны в моей статье: Полезные идиомы многопоточности С++. Только у меня еще круче: можно также реализовать RW locks через длинную стрелку --->. Еще предлагаю посмотреть видео видео со встречи C++ User Group в Санкт-Петербурге.
Похоже, вы правы. Хотя некоторые отличия всё-таки есть, но суть одна, согласен. Признаюсь, я с самого начала не надеялся, что изобрёл что-то принципиально новое. Целью было скорее обратить внимание на неплохую, ИМХО, идиому тех, кто раньше про неё не слышал. Ну и ещё я надеялся, что кому-нибудь из новичков, возможно, будет полезно почитать про пошаговый процесс разработки класса с желаемой функциональностью, где более-менее понятно описано, почему при разработки используются те или иные решения. Видео обязательно посмотрю, как доберусь домой, спасибо.
Ничего подобного. Здесь подход более правильный, lock явный. И никаких длинных стрелок!
У моего подхода больше возможностей: можно как явно, так и неявно. А любую из этих возможностей при желании можно отрубить. Так что идеологически — более полноценный подход.
Больше возможностей — не значит лучше
Простое лучше сложного
Явное лучше неявного

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

struct c {int x};
AnLock<c> v;
v->x = foo(v->x); // deadlock or not atomic depending on compiler
Вот вам еще одна ошибка, которая невозможна при явном локе
Т.е. так нельзя написать:

v.lock()->x = foo(f.lock()->x)
?

А вообще, когда я писал про возможности, то имел в виду, что разработчик может оставить в своем проекте только те возможности, которые ему нужны. И в данном случае я считаю, что иметь выбор лучше его отсутствия. Более того, если посмотреть мою презентацию, то там я рассказываю про то, как можно избегать дедлоков при работе сразу с несколькими мьютексами, а здесь вообще про это ничего не сказано. Далее: при работе с разделяемым объектом не синглтоном возникает вопрос о времени жизни со всеми вытикающими. Как такие объекты класть в контейнер? Как изменять? Как работать сразу с несколькими объектами из разных контейнеров? Тут не очень понятно.
Для того, чтобы написать
 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

У этой статьи на мой взгляд вполне внятные рамки — предложить реализацию конкретного шаблона. И с этой задачей статья отлично справляется. Не нужно сюда приплетать разделяемые объекты.
Здесь стоит подумать о 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)...) { }
Да вы правы, спасибо. Поправлю в репозитории.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории