По дизайну std::mutex
не подлежит перемещению или копированию. Это означает, что класс, A
содержащий мьютекс, не получит конструктор перемещения по умолчанию.
Как сделать этот тип A
перемещаемым потокобезопасным способом?
c++
mutex
move-constructor
Джек Саббат
источник
источник
std::lock_guard
область видимости метода is.Ответы:
Начнем с небольшого кода:
class A { using MutexType = std::mutex; using ReadLock = std::unique_lock<MutexType>; using WriteLock = std::unique_lock<MutexType>; mutable MutexType mut_; std::string field1_; std::string field2_; public: ...
Я поместил туда несколько довольно многообещающих псевдонимов типов, которые мы не будем использовать в C ++ 11, но станут намного более полезными в C ++ 14. Наберитесь терпения, мы доберемся туда.
Ваш вопрос сводится к следующему:
Начнем с конструктора перемещения.
Конструктор перемещения
Обратите внимание, что член
mutex
был сделанmutable
. Строго говоря, это не обязательно для участников перемещения, но я предполагаю, что вам также нужны копирующие члены. Если это не так, нет необходимости делать мьютексmutable
.При строительстве
A
не нужно блокироватьthis->mut_
. Но вам нужно заблокироватьmut_
объект, из которого вы строите (переместить или скопировать). Сделать это можно так:A(A&& a) { WriteLock rhs_lk(a.mut_); field1_ = std::move(a.field1_); field2_ = std::move(a.field2_); }
Обратите внимание, что мы должны были
this
сначала создать элементы по умолчанию , а затем присвоить им значения только послеa.mut_
блокировки.Переместить назначение
Оператор присваивания перемещения значительно сложнее, потому что вы не знаете, обращается ли какой-либо другой поток к левой или правой стороне выражения присваивания. И вообще, вам нужно остерегаться следующего сценария:
// Thread 1 x = std::move(y); // Thread 2 y = std::move(x);
Вот оператор присваивания перемещения, который правильно защищает описанный выше сценарий:
A& operator=(A&& a) { if (this != &a) { WriteLock lhs_lk(mut_, std::defer_lock); WriteLock rhs_lk(a.mut_, std::defer_lock); std::lock(lhs_lk, rhs_lk); field1_ = std::move(a.field1_); field2_ = std::move(a.field2_); } return *this; }
Обратите внимание, что нужно использовать
std::lock(m1, m2)
для блокировки двух мьютексов, а не просто блокировать их один за другим. Если вы заблокируете их один за другим, тогда, когда два потока назначат два объекта в противоположном порядке, как показано выше, вы можете получить тупик. Дело вstd::lock
том, чтобы избежать этого тупика.Копировать конструктор
Вы не спрашивали о копировальных членах, но мы могли бы поговорить о них сейчас (если не вы, они кому-нибудь понадобятся).
A(const A& a) { ReadLock rhs_lk(a.mut_); field1_ = a.field1_; field2_ = a.field2_; }
Конструктор копирования очень похож на конструктор перемещения, за исключением того,
ReadLock
что вместоWriteLock
. В настоящее время это оба псевдонима,std::unique_lock<std::mutex>
поэтому на самом деле это не имеет никакого значения.Но в C ++ 14 у вас будет возможность сказать следующее:
using MutexType = std::shared_timed_mutex; using ReadLock = std::shared_lock<MutexType>; using WriteLock = std::unique_lock<MutexType>;
Это может быть оптимизация, но не определенно. Вам нужно будет измерить, чтобы определить, так ли это. Но с этим изменением можно копировать конструкцию из одной и той же правой руки в несколько потоков одновременно. Решение C ++ 11 заставляет вас делать такие потоки последовательными, даже если правая сторона не изменяется.
Копировать присвоение
Для полноты, вот оператор присваивания копии, который должен быть достаточно понятным после прочтения всего остального:
A& operator=(const A& a) { if (this != &a) { WriteLock lhs_lk(mut_, std::defer_lock); ReadLock rhs_lk(a.mut_, std::defer_lock); std::lock(lhs_lk, rhs_lk); field1_ = a.field1_; field2_ = a.field2_; } return *this; }
И так далее.
Любые другие члены или свободные функции, которые обращаются
A
к состоянию, также должны быть защищены, если вы ожидаете, что несколько потоков смогут вызывать их одновременно. Например, вотswap
:friend void swap(A& x, A& y) { if (&x != &y) { WriteLock lhs_lk(x.mut_, std::defer_lock); WriteLock rhs_lk(y.mut_, std::defer_lock); std::lock(lhs_lk, rhs_lk); using std::swap; swap(x.field1_, y.field1_); swap(x.field2_, y.field2_); } }
Обратите внимание, что если вы просто зависите от
std::swap
выполнения задания, блокировка будет с неправильной степенью детализации, блокировка и разблокировка между тремя движениями, которыеstd::swap
будут выполняться внутри.Действительно, размышление
swap
может дать вам представление об API, который может потребоваться для обеспечения «поточно-ориентированного»A
, который в целом будет отличаться от «небезопасного для потоков» API из-за проблемы «блокирующей гранулярности».Также обратите внимание на необходимость защиты от «самостоятельной замены». «Самостоятельная замена» не должна применяться. Без самопроверки можно было бы рекурсивно заблокировать один и тот же мьютекс. Эту проблему также можно решить без самопроверки, используя
std::recursive_mutex
forMutexType
.Обновить
В комментариях ниже Якк очень недоволен тем, что ему приходится создавать объекты по умолчанию в конструкторах копирования и перемещения (и он прав). Если вы достаточно серьезно относитесь к этой проблеме, настолько, что готовы потратить на нее память, вы можете избежать этого следующим образом:
Добавьте любые типы блокировок, которые вам нужны в качестве членов данных. Эти члены должны предшествовать защищаемым данным:
mutable MutexType mut_; ReadLock read_lock_; WriteLock write_lock_; // ... other data members ...
А затем в конструкторах (например, конструкторе копирования) сделайте следующее:
A(const A& a) : read_lock_(a.mut_) , field1_(a.field1_) , field2_(a.field2_) { read_lock_.unlock(); }
К сожалению, Якк стер свой комментарий до того, как я успел завершить это обновление. Но он заслуживает похвалы за то, что поднял эту проблему и нашел решение в этом ответе.
Обновление 2
И dyp придумал хорошее предложение:
A(const A& a) : A(a, ReadLock(a.mut_)) {} private: A(const A& a, ReadLock rhs_lk) : field1_(a.field1_) , field2_(a.field2_) {}
источник
mutexes
в типы классов - это не «единственный верный способ». Это инструмент в наборе инструментов, и если вы хотите его использовать, вот как.Учитывая, что, похоже, нет хорошего, чистого и простого способа ответить на это - решение Антона, которое я считаю правильным, но определенно спорным, если не появится лучший ответ, я бы рекомендовал поместить такой класс в кучу и присмотреть за ним через
std::unique_ptr
:auto a = std::make_unique<A>();
Его теперь полностью подвижный тип и любой, кто имеет замок на внутренней взаимной блокировки в то время как движение происходит по-прежнему безопасно, даже если его спорны ли это хорошая вещь, чтобы сделать
Если вам нужна семантика копирования, просто используйте
auto a2 = std::make_shared<A>();
источник
Это перевернутый ответ. Вместо того, чтобы встраивать «эти объекты должны быть синхронизированы» в качестве основы типа, вставьте его под любой тип.
Вы имеете дело с синхронизированным объектом совсем по-другому. Одна большая проблема - вам нужно беспокоиться о взаимоблокировках (блокировке нескольких объектов). Он также никогда не должен быть вашей «версией объекта по умолчанию»: синхронизированные объекты предназначены для объектов, которые будут конкурировать, и ваша цель должна состоять в том, чтобы минимизировать конкуренцию между потоками, а не скрывать ее.
Но синхронизация объектов по-прежнему полезна. Вместо наследования от синхронизатора мы можем написать класс, который объединяет произвольный тип в синхронизацию. Пользователи должны перепрыгнуть через несколько обручей, чтобы выполнить операции с объектом теперь, когда он синхронизирован, но они не ограничены каким-то ограниченным набором вручную закодированных операций с объектом. Они могут объединить несколько операций над объектом в одну или выполнить операцию над несколькими объектами.
Вот синхронизированная оболочка вокруг произвольного типа
T
:template<class T> struct synchronized { template<class F> auto read(F&& f) const&->std::result_of_t<F(T const&)> { return access(std::forward<F>(f), *this); } template<class F> auto read(F&& f) &&->std::result_of_t<F(T&&)> { return access(std::forward<F>(f), std::move(*this)); } template<class F> auto write(F&& f)->std::result_of_t<F(T&)> { return access(std::forward<F>(f), *this); } // uses `const` ness of Syncs to determine access: template<class F, class... Syncs> friend auto access( F&& f, Syncs&&... syncs )-> std::result_of_t< F(decltype(std::forward<Syncs>(syncs).t)...) > { return access2( std::index_sequence_for<Syncs...>{}, std::forward<F>(f), std::forward<Syncs>(syncs)... ); }; synchronized(synchronized const& o):t(o.read([](T const&o){return o;})){} synchronized(synchronized && o):t(std::move(o).read([](T&&o){return std::move(o);})){} // special member functions: synchronized( T & o ):t(o) {} synchronized( T const& o ):t(o) {} synchronized( T && o ):t(std::move(o)) {} synchronized( T const&& o ):t(std::move(o)) {} synchronized& operator=(T const& o) { write([&](T& t){ t=o; }); return *this; } synchronized& operator=(T && o) { write([&](T& t){ t=std::move(o); }); return *this; } private: template<class X, class S> static auto smart_lock(S const& s) { return std::shared_lock< std::shared_timed_mutex >(s.m, X{}); } template<class X, class S> static auto smart_lock(S& s) { return std::unique_lock< std::shared_timed_mutex >(s.m, X{}); } template<class L> static void lock(L& lockable) { lockable.lock(); } template<class...Ls> static void lock(Ls&... lockable) { std::lock( lockable... ); } template<size_t...Is, class F, class...Syncs> friend auto access2( std::index_sequence<Is...>, F&&f, Syncs&&...syncs)-> std::result_of_t< F(decltype(std::forward<Syncs>(syncs).t)...) > { auto locks = std::make_tuple( smart_lock<std::defer_lock_t>(syncs)... ); lock( std::get<Is>(locks)... ); return std::forward<F>(f)(std::forward<Syncs>(syncs).t ...); } mutable std::shared_timed_mutex m; T t; }; template<class T> synchronized< T > sync( T&& t ) { return {std::forward<T>(t)}; }
Включены функции C ++ 14 и C ++ 1z.
это предполагает, что
const
операции безопасны для нескольких считывателей (чтоstd
предполагают контейнеры).Использование выглядит так:
synchronized<int> x = 7; x.read([&](auto&& v){ std::cout << v << '\n'; });
для
int
синхронизированного доступа.Я бы не советовал иметь
synchronized(synchronized const&)
. Это редко нужно.Если вам нужно
synchronized(synchronized const&)
, у меня возникнет соблазн заменить егоT t;
наstd::aligned_storage
, позволяя создавать строительство вручную, и выполнять ручное разрушение. Это позволяет правильно управлять сроком службы.Если этого не сделать, мы могли бы скопировать источник
T
, а затем прочитать из него:synchronized(synchronized const& o): t(o.read( [](T const&o){return o;}) ) {} synchronized(synchronized && o): t(std::move(o).read( [](T&&o){return std::move(o);}) ) {}
по назначению:
synchronized& operator=(synchronized const& o) { access([](T& lhs, T const& rhs){ lhs = rhs; }, *this, o); return *this; } synchronized& operator=(synchronized && o) { access([](T& lhs, T&& rhs){ lhs = std::move(rhs); }, *this, std::move(o)); return *this; } friend void swap(synchronized& lhs, synchronized& rhs) { access([](T& lhs, T& rhs){ using std::swap; swap(lhs, rhs); }, *this, o); }
версии размещения и выровненного хранилища немного запутаннее. Большая часть доступа к
t
будет заменена функцией-членомT&t()
иT const&t()const
, за исключением строительства, где вам придется перепрыгивать через некоторые обручи.Создавая
synchronized
оболочку вместо части класса, все, что мы должны гарантировать, это то, что класс внутренне уважаетconst
как многопоточность и записывает его в однопоточном режиме.В редких случаях, когда нам нужен синхронизированный экземпляр, мы прыгаем через обручи, как показано выше.
Приносим извинения за возможные опечатки в вышеуказанном. Наверное, есть.
Дополнительным преимуществом вышеизложенного является то, что n-арные произвольные операции с
synchronized
объектами (одного и того же типа) работают вместе, без необходимости их жесткого кодирования заранее. Добавьте объявление друга, и n-арныеsynchronized
объекты нескольких типов могут работать вместе. Вaccess
таком случае мне, возможно, придется перестать быть постоянным другом, чтобы иметь дело с конфликтами перегрузки.живой пример
источник
Использование мьютексов и семантики перемещения C ++ - отличный способ безопасно и эффективно передавать данные между потоками.
Представьте себе поток «производителя», который создает пакеты строк и предоставляет их (одному или нескольким) потребителям. Эти пакеты могут быть представлены объектом, содержащим (потенциально большие)
std::vector<std::string>
объекты. Мы абсолютно хотим «переместить» внутреннее состояние этих векторов в их потребителей без ненужного дублирования.Вы просто узнаете мьютекс как часть объекта, а не как часть состояния объекта. То есть вы не хотите перемещать мьютекс.
Какая блокировка вам нужна, зависит от вашего алгоритма или от того, насколько обобщены ваши объекты и какой диапазон использования вы разрешаете.
Если вы только когда - либо перейти от общего состояния «производителя» объекта к локальному потоку «потребляя» объект , который вы могли бы быть в порядке , чтобы только зафиксировать двигающийся от объекта.
Если это более общий дизайн, вам нужно будет заблокировать оба. В таком случае вам нужно подумать о мертвой блокировке.
Если это потенциальная проблема, используйте
std::lock()
для получения блокировок на обоих мьютексах без взаимоблокировок.http://en.cppreference.com/w/cpp/thread/lock
В заключение вам необходимо убедиться, что вы понимаете семантику перемещения. Напомним, что перемещенный объект остается в допустимом, но неизвестном состоянии. Вполне возможно, что у потока, не выполняющего перемещение, есть веская причина для попытки доступа к перемещенному объекту, когда он может найти это допустимое, но неизвестное состояние.
Опять же, мой продюсер просто перебирает струны, а потребитель снимает с себя всю нагрузку. В этом случае каждый раз, когда производитель пытается добавить к вектору, он может найти вектор непустым или пустым.
Короче говоря, если потенциальный одновременный доступ к перемещаемому объекту составляет запись, скорее всего, все в порядке. Если это означает чтение, подумайте, почему нормально читать произвольное состояние.
источник
Прежде всего, если вы хотите переместить объект, содержащий мьютекс, что-то не так с вашим дизайном.
Но если вы все равно решите это сделать, вам нужно создать новый мьютекс в конструкторе перемещения, например:
// movable struct B{}; class A { B b; std::mutex m; public: A(A&& a) : b(std::move(a.b)) // m is default-initialized. { } };
Это потокобезопасный, потому что конструктор перемещения может безопасно предполагать, что его аргумент больше нигде не используется, поэтому блокировка аргумента не требуется.
источник
a.mutex
заблокирован: вы теряете это состояние. -1A a; A a2(std::move(a)); do some stuff with a
.new
запустить экземпляр и поместить его вstd::unique_ptr
- это кажется более чистым и вряд ли приведет к проблемам. Хороший вопрос.