Ответ 1
Есть две отдельные проблемы (безопасность потоков и безопасность исключений), и лучше всего их решать отдельно. Чтобы позволить конструкторам, принимающим другой объект в качестве аргумента для получения блокировки при инициализации членов, необходимо в любом случае разделить элементы данных на отдельный класс: таким образом блокировка может быть получена, когда подобъект инициализирован, и класс, поддерживающий фактические данные может игнорировать любые проблемы concurrency. Таким образом, класс будет разделен на две части: class A
для решения проблем concurrency и class A_unlocked
для сохранения данных. Поскольку функции-члены A_unlocked
не имеют защиты concurrency, они не должны непосредственно отображаться в интерфейсе, и, таким образом, A_unlocked
становится частным членом A
.
Создание безопасного оператора присваивания является прямым, используя конструктор копирования. Аргумент копируется и члены меняются местами:
A_unlocked& A_unlocked::operator= (A_unlocked const& other) {
A_unlocked(other).swap(*this);
return *this;
}
Конечно, это означает, что реализован подходящий конструктор копирования и элемент swap()
. Работа с распределением нескольких ресурсов, например, несколькими объектами, выделенными в куче, проще всего сделать с помощью подходящего обработчика ресурсов для каждого из объектов. Без использования обработчиков ресурсов очень быстро становится беспорядочно очищать все ресурсы в случае возникновения исключения. Для сохранения памяти, выделенной кучей std::unique_ptr<T>
(или std::auto_ptr<T>
, если вы не можете использовать С++ 2011), является подходящим выбором. Приведенный ниже код просто копирует объекты, указывающие на объекты, хотя не так много смысла выделять объекты в куче, а не создавать их. В реальном примере объекты, вероятно, будут реализовывать метод clone()
или какой-либо другой механизм для создания объекта с правильным типом:
class A_unlocked {
private:
std::unique_ptr<B> pb;
std::unique_ptr<C> pc;
// ...
public:
A_unlocked(/*...*/);
A_unlocked(A_unlocked const& other);
A_unlocked& operator= (A_unlocked const& other);
void swap(A_unlocked& other);
// ...
};
A_unlocked::A_unlocked(A_unlocked const& other)
: pb(new B(*other.pb))
, pc(new C(*other.pc))
{
}
void A_unlocked::swap(A_unlocked& other) {
using std::swap;
swap(this->pb, other.pb);
swap(this->pc, other.pc);
}
Для бита безопасности потока необходимо знать, что никакой другой поток не возится с скопированным объектом. Способ сделать это - использовать мьютекс. То есть class A
выглядит примерно так:
class A {
private:
mutable std::mutex d_mutex;
A_unlocked d_data;
public:
A(/*...*/);
A(A const& other);
A& operator= (A const& other);
// ...
};
Обратите внимание, что всем элементам A
необходимо будет выполнить некоторую защиту concurrency, если объекты типа A
предназначены для использования без внешней блокировки. Поскольку мьютекс, используемый для защиты от одновременного доступа, на самом деле не является частью состояния объекта, но его необходимо изменить даже при чтении состояния объекта, он сделан mutable
. При этом создание конструктора копирования выполняется прямо:
A::A(A const& other)
: d_data((std::unique_lock<std::mutex>(other.d_mutex), other.d_data)) {
}
Это блокирует аргумент mutex и делегирует его конструктору-экземпляру. Блокировка автоматически освобождается в конце выражения, независимо от того, была ли копия успешной или выбрала исключение. Строящийся объект не нуждается в какой-либо блокировке, потому что еще нет способа узнать об этом объекте другой поток.
Основная логика оператора присваивания также просто делегирует базу, используя оператор присваивания. Сложный бит состоит в том, что есть два мьютекса, которые необходимо заблокировать: одно для объекта, которому назначено, и одно для аргумента. Поскольку другой поток может назначать два объекта в обратном порядке, существует вероятность блокировки. Удобно, что стандартная библиотека С++ предоставляет алгоритм std::lock()
, который обеспечивает блокировки соответствующим образом, что позволяет избежать блокировок. Один из способов использования этого алгоритма - передать разблокированные объекты std::unique_lock<std::mutex>
, по одному для каждого мьютекса, который необходимо приобрести:
A& A::operator= (A const& other) {
if (this != &other) {
std::unique_lock<std::mutex> guard_this(this->d_mutex, std::defer_lock);
std::unique_lock<std::mutex> guard_other(other.d_mutex, std::defer_lock);
std::lock(guard_this, guard_other);
*this->d_data = other.d_data;
}
return *this;
}
Если в любой момент во время назначения генерируется исключение, защитные блокировки освободят мьютексы, а обработчики ресурсов освободят любой выделенный ресурс. Таким образом, вышеупомянутый подход реализует сильную гарантию исключения. Интересно, что для назначения копии необходимо выполнить проверку самонаведения, чтобы предотвратить блокировку одного и того же мьютекса дважды. Обычно я утверждаю, что необходимая проверка самонаведения является указанием на то, что оператор присваивания не является безопасным для исключений, но я думаю, что приведенный выше код является безопасным для исключения.
Это основная переписка с ответом. Более ранние версии этого ответа были либо подвержены потерянному обновлению, либо мертвому замку. Спасибо Якку за то, что он указал на проблемы. Хотя результат решения проблем включает в себя больше кода, я думаю, что каждая отдельная часть кода на самом деле проще и может быть исследована на правильность.