Начнем с немного кода:
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.
Ваш вопрос сводится к следующему:
Как написать конструктор перемещения и переместить оператор присваивания для этого класса?
blockquote>Начнем с конструктора перемещения.
Move Constructor
Обратите внимание, что элемент
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_
.Назначение перемещения
Оператор присваивания перемещения значительно сложнее, потому что вы не знаете, обращается ли какой-либо другой поток к lhs или rhs выражения присваивания. И вообще, вам нужно защититься от следующего сценария:
// 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 заставляет вас делать такие потоки последовательными, даже если rhs не изменяется.
Назначение копирования
Для полноты здесь оператор присваивания копий, который должен быть достаточно понятным после прочтения обо всем остальном:
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 из-за проблемы «блокировки гранулярности».Также обратите внимание на необходимость защиты от «самообмена». «self-swap» должен быть не-op. Без самопроверки рекурсивно блокирует один и тот же мьютекс. Это также можно было бы решить без самопроверки с помощью
std::recursive_mutex
дляMutexType
.Обновить
В комментариях ниже Якк довольно недоволен необходимостью по умолчанию строить вещи в копировать и перемещать конструкторы (и у него есть точка). Если вы достаточно решительно относитесь к этой проблеме, так что вы готовы потратить на нее память, вы можете избежать ее так:
- Добавьте все типы блокировок, которые вам нужны в качестве элементов данных. Эти члены должны находиться перед защищаемыми данными:
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(); }
К сожалению, Yakk удалил свой комментарий, прежде чем у меня была возможность завершить это обновление.
Обновление 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_) {}