У меня есть обертка для какого-то унаследованного кода.
class A{
L* impl_; // the legacy object has to be in the heap, could be also unique_ptr
A(A const&) = delete;
L* duplicate(){L* ret; legacy_duplicate(impl_, &L); return ret;}
... // proper resource management here
};
В этом унаследованном коде функция, которая «дублирует» объект, не является поточно-ориентированной (при вызове того же первого аргумента), поэтому она не отмечена const
в оболочке. Я предполагаю следующие современные правила: https://herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/
Это duplicate
похоже на хороший способ реализовать конструктор копирования, за исключением деталей, которые не являются const
. Поэтому я не могу сделать это напрямую:
class A{
L* impl_; // the legacy object has to be in the heap
A(A const& other) : L{other.duplicate()}{} // error calling a non-const function
L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
Так какой же выход из этой парадоксальной ситуации?
(Допустим также, что legacy_duplicate
это не потокобезопасно, но я знаю, что оставляет объект в исходном состоянии, когда он выходит. Будучи C-функцией, поведение только документируется, но не имеет понятия постоянства.)
Я могу думать о многих возможных сценариях:
(1) Одна из возможностей заключается в том, что нет способа реализовать конструктор копирования с обычной семантикой вообще. (Да, я могу переместить объект, и это не то, что мне нужно.)
(2) С другой стороны, копирование объекта по своей природе не является потокобезопасным в том смысле, что копирование простого типа может найти источник в полу-модифицированном состоянии, поэтому я могу просто пойти дальше и, возможно, сделать это,
class A{
L* impl_;
A(A const& other) : L{const_cast<A&>(other).duplicate()}{} // error calling a non-const function
L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
(3) или даже просто объявить duplicate
const и лгать о безопасности потоков во всех контекстах. (В конце концов, устаревшая функция не заботится, const
поэтому компилятор даже не будет жаловаться.)
class A{
L* impl_;
A(A const& other) : L{other.duplicate()}{}
L* duplicate() const{L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
(4) Наконец, я могу следовать логике и создать конструктор копирования, который принимает неконстантный аргумент.
class A{
L* impl_;
A(A const&) = delete;
A(A& other) : L{other.duplicate()}{}
L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
Оказывается, это работает во многих контекстах, потому что эти объекты обычно не const
.
Вопрос в том, является ли это действительным или распространенным маршрутом?
Я не могу назвать их, но я интуитивно ожидаю множество проблем в будущем с использованием неконстантного конструктора копирования. Вероятно, это не будет квалифицироваться как тип значения из-за этой тонкости.
(5) Наконец, хотя это кажется излишним и может иметь большие затраты времени выполнения, я мог бы добавить мьютекс:
class A{
L* impl_;
A(A const& other) : L{other.duplicate_locked()}{}
L* duplicate(){
L* ret; legacy_duplicate(impl_, &ret); return ret;
}
L* duplicate_locked() const{
std::lock_guard<std::mutex> lk(mut);
L* ret; legacy_duplicate(impl_, &ret); return ret;
}
mutable std::mutex mut;
};
Но принуждение к этому похоже на пессимизацию и делает класс больше. Я не уверена. В настоящее время я склоняюсь к (4) или (5) или их комбинации.
РЕДАКТИРОВАТЬ 1:
Другой вариант:
(6) Забудьте обо всех бессмысленных повторяющихся функциях-членах и просто вызовите legacy_duplicate
конструктор и объявите, что конструктор копирования не является потокобезопасным. (И, если необходимо, создайте другую поточно-ориентированную версию этого типа A_mt
)
class A{
L* impl_;
A(A const& other){legacy_duplicate(other.impl_, &impl_);}
};
РЕДАКТИРОВАТЬ 2:
Это может быть хорошей моделью для того, что делает унаследованная функция. Обратите внимание, что касаясь ввода, вызов не является потокобезопасным по отношению к значению, представленному первым аргументом.
void legacy_duplicate(L* in, L** out){
*out = new L{};
char tmp = in[0];
in[0] = tmp;
std::memcpy(*out, in, sizeof *in); return;
}
РЕДАКТИРОВАТЬ 3:
Я недавно узнал, что std::auto_ptr
была похожая проблема с неконстантным конструктором копирования. Эффект был в том, что auto_ptr
нельзя было использовать внутри контейнера. https://www.quantstart.com/articles/STL-Containers-and-Auto_ptrs-Why-They-Dont-Mix/
L
которое изменяется путем создания новогоL
экземпляра? Если нет, то почему вы считаете, что эта операция не является поточно-ориентированной?legacy_duplicate
не может быть вызвана с одним и тем же первым аргументом из двух разных потоков.const
самом деле значит. :-) Я бы не подумал дважды о том, чтобы взятьconst&
в свой экземпляр ctor, если я не изменяюother
. Я всегда думаю о безопасности потоков как о чем-то, что добавляется к тому, что необходимо получить из нескольких потоков, через инкапсуляцию, и я действительно жду ответов.Ответы:
Я бы просто включил обе ваши опции (4) и (5), но явно включил небезопасное поведение, когда вы считаете, что это необходимо для производительности.
Вот полный пример.
Вывод:
Это следует руководству по стилю Google, в котором
const
сообщается о безопасности потоков, но код, вызывающий ваш API, может отказаться, используяconst_cast
источник
legacy_duplicate
могла бы бытьvoid legacy_duplicate(L* in, L** out) { *out = new L{}; char tmp = in[0]; /*some weird call here*/; in[0] = tmp; std::memcpy(*out, in, sizeof *in); return; }
(то есть неконстантнойin
)A a2(a1)
может пытаться быть потокобезопасным (или быть удаленным) иA a2(const_cast<A&>(a1))
вообще не пытаться быть потокобезопасным.A
как в поточно-безопасном, так и в поточно-небезопасном контекстах, вы должны перетащитьconst_cast
код вызова, чтобы было ясно, где известно, что поточная безопасность нарушена. Можно добавить дополнительную безопасность за API (мьютекс), но нельзя скрывать небезопасность (const_cast).TLDR: Исправьте реализацию вашей функции дублирования, или ввести мьютекс (или какое - то более подходящего запорное устройство, возможно спинлок, или убедитесь , что мьютекс настроен на спину , прежде чем делать что - либо тяжелее) на данный момент , а затем зафиксировать осуществление дублирования и удалите блокировку, когда блокировка фактически становится проблемой.
Я думаю, что ключевой момент, на который следует обратить внимание, это то, что вы добавляете функцию, которой раньше не было: возможность дублировать объект из нескольких потоков одновременно.
Очевидно, что в описанных вами условиях это было бы ошибкой - условием гонки, если бы вы делали это раньше, без использования какой-либо внешней синхронизации.
Поэтому любое использование этой новой функции будет тем, что вы добавляете в свой код, а не наследуете как существующую функциональность. Вы должны быть тем, кто знает, будет ли добавление дополнительной блокировки действительно дорогостоящим - в зависимости от того, как часто вы собираетесь использовать эту новую функцию.
Кроме того, исходя из воспринимаемой сложности объекта - из-за особой обработки, которую вы ему оказываете, я собираюсь предположить, что процедура дублирования не является тривиальной, поэтому уже довольно дорогой с точки зрения производительности.
Исходя из вышеизложенного, у вас есть два пути, по которым вы можете следовать:
A) Вы знаете, что копирование этого объекта из нескольких потоков не будет происходить достаточно часто, чтобы накладные расходы на дополнительную блокировку были дорогостоящими - возможно, тривиально дешевыми, по крайней мере, учитывая, что существующая процедура дублирования сама по себе достаточно дорога, если вы спин-блокировка / предварительное вращение мьютекса, и в этом нет споров.
Б) Вы подозреваете, что копирование из нескольких потоков будет происходить достаточно часто, чтобы возникла дополнительная блокировка. Тогда у вас действительно есть только один вариант - исправить код дублирования. Если вы не исправите это, вам все равно понадобится блокировка, будь то на этом уровне абстракции или где-то еще, но она понадобится вам, если вы не хотите ошибок - и, как мы установили, на этом пути вы предполагаете такая блокировка будет слишком дорогой, поэтому единственный вариант - исправить код дублирования.
Я подозреваю, что вы действительно находитесь в ситуации A, и просто добавление спин-блокировки / вращающегося мьютекса, который практически не снижает производительности при неоспоримых результатах, будет работать очень хорошо (хотя и не забывайте тестировать его).
Теоретически существует другая ситуация:
C) В отличие от кажущейся сложности функции дублирования, она на самом деле тривиальна, но по какой-то причине не может быть исправлена; это настолько тривиально, что даже неоспоримая спин-блокировка приводит к недопустимому снижению производительности при дублировании; дублирование в параллельных потоках используется редко; Дублирование в одном потоке используется постоянно, что делает снижение производительности абсолютно неприемлемым.
В этом случае я предлагаю следующее: объявить конструкторы / операторы копирования по умолчанию удаленными, чтобы никто не мог их случайно использовать. Создайте два явно вызываемых метода дублирования, потокобезопасный и небезопасный поток; Заставьте своих пользователей вызывать их явно, в зависимости от контекста. Опять же, нет другого способа достичь приемлемой производительности одного потока и безопасной многопоточности, если вы действительно находитесь в такой ситуации и просто не можете исправить существующую реализацию дублирования. Но я чувствую, что это маловероятно, что вы на самом деле.
Просто добавьте этот мьютекс / спин-блокировку и тест.
источник
std::mutex
? Функция дублирования не секрет, я не упомянул ее, чтобы держать проблему на высоком уровне и не получать ответы о MPI. Но так как вы зашли так глубоко, я могу дать вам больше деталей. Унаследованная функция - это,MPI_Comm_dup
и эффективная безопасность без потоков описана здесь (я подтвердил это) github.com/pmodels/mpich/issues/3234 . Вот почему я не могу исправить дубликаты. (Также, если я добавлю мьютекс, у меня будет соблазн сделать все вызовы MPI потоко-безопасными.)