Скопировать конструктор с неконстантным аргументом, предложенным правилами безопасности потока? - PullRequest
9 голосов
/ 06 февраля 2020

У меня есть обертка для некоторого устаревшего кода.

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 -функцией, поведение только документировано, но не имеет понятия постоянства.)

Я могу придумать много возможных scenar ios:

(1) Одна из возможностей состоит в том, что невозможно вообще реализовать конструктор копирования с обычной семантикой. (Да, я могу переместить объект, и это не то, что мне нужно.)

(2) С другой стороны, копирование объекта по своей сути не является потокобезопасным в том смысле, что копирование простого типа может найти источник в полу-модифицированном состоянии, поэтому я могу просто go переслать и сделать это, возможно,

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 и l ie о безопасности потоков во всех контекстах. (В конце концов, устаревшая функция не заботится о const, поэтому компилятор даже не будет жаловаться.)

class A{
   L* impl_;
   A(A const& other) : L{other.duplicate()}{}
   L* duplicate() const{L* ret; legacy_duplicate(impl_, &ret); return ret;}
};

(4) Наконец, я могу следовать логике c и создайте конструктор копирования, который принимает неконстантный аргумент.

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/

Ответы [ 2 ]

0 голосов
/ 20 февраля 2020

TLDR: либо исправьте реализацию вашей функции дублирования, либо введите мьютекс (или какое-нибудь более подходящее блокирующее устройство, возможно, спин-блокировку, либо убедитесь, что ваш мьютекс настроен на вращение, прежде чем делать что-то более тяжелое) на данный момент , затем исправьте реализацию дублирования и снимите блокировку, когда блокировка фактически становится проблемой.

Я думаю, что ключевым моментом, на который следует обратить внимание, является то, что вы добавляете функцию, которой раньше не было: возможность дублирование объекта из нескольких потоков одновременно.

Очевидно, что при описанных вами условиях это было бы ошибкой - условием гонки, если бы вы делали это раньше, без использования какого-либо вида внешняя синхронизация.

Поэтому любое использование этой новой функции будет тем, что вы добавляете в свой код, а не наследуете как существующую функциональность. Вы должны быть тем, кто знает , будет ли добавление дополнительной блокировки действительно дорогостоящим - в зависимости от того, как часто вы собираетесь использовать эту новую функцию.

Кроме того, исходя из предполагаемой сложности объекта - благодаря особой обработке, которую вы ему предоставляете, я собираюсь предположить, что процедура дублирования не является тривиальной, поэтому уже довольно дорогой с точки зрения производительности.

Исходя из вышеизложенного, у вас есть два пути, по которым вы можете следовать:

A) Вы знаете, что копирование этого объекта из нескольких потоков не будет происходить достаточно часто, чтобы накладные расходы на дополнительную блокировку были дорогостоящими - возможно, тривиально дешевыми, по крайней мере, учитывая, что существующая процедура дублирования достаточно затратна сама по себе, если вы используете мьютекс с спин-блокировкой / предварительным вращением и в этом нет разногласий.

B) Вы подозреваете, что копирование из нескольких потоков будет происходить достаточно часто для дополнительная блокировка, чтобы быть проблемой. Тогда у вас действительно есть только один вариант - исправить код дублирования. Если вы не исправите это, вам все равно понадобится блокировка, будь то на этом уровне абстракции или где-то еще, но она понадобится вам, если вы не хотите ошибок - и, как мы установили, на этом пути вы предполагаете такая блокировка будет слишком дорогой, поэтому единственный вариант - исправить код дублирования.

Я подозреваю, что вы действительно находитесь в ситуации А, и просто добавляете спин-блокирующий / вращающийся мьютекс, который практически не снижает производительность, когда неоспоримо, будет работать просто отлично (хотя не забывайте тестировать его).

Теоретически существует другая ситуация:

C) В отличие от кажущейся сложности функции дублирования , это на самом деле тривиально, но не может быть исправлено по какой-то причине; это настолько тривиально, что даже неоспоримая спин-блокировка приводит к недопустимому снижению производительности при дублировании; дублирование в параллельных потоках используется редко; Дублирование в одном потоке используется постоянно, что делает снижение производительности абсолютно неприемлемым.

В этом случае я предлагаю следующее: объявить конструкторы / операторы копирования по умолчанию удаленными, чтобы никто не мог их случайно использовать. Создайте два явно вызываемых метода дублирования, потокобезопасный и небезопасный поток; Сделайте так, чтобы ваши пользователи вызывали их явно, в зависимости от контекста. Опять же, нет другого способа достичь приемлемой производительности одного потока и безопасной многопоточности, если вы действительно находитесь в такой ситуации и просто не можете исправить существующую реализацию дублирования. Но я чувствую, что это маловероятно, что вы на самом деле.

Просто добавьте этот мьютекс / спин-блокировку и тест.

0 голосов
/ 19 февраля 2020

Я бы просто включил обе ваши опции (4) и (5), но явно включил небезопасное поведение, когда вы считаете, что это необходимо для производительности.

Вот полный пример.

#include <cstdlib>
#include <thread>

struct L {
  int val;
};

void legacy_duplicate(const L* in, L** out) {
  *out = new L{};
  std::memcpy(*out, in, sizeof *in);
  return;
}

class A {
 public:
  A(L* l) : impl_{l} {}
  A(A const& other) : impl_{other.duplicate_locked()} {}

  A copy_unsafe_for_multithreading() { return {duplicate()}; }

  L* impl_;

  L* duplicate() {
    printf("in duplicate\n");
    L* ret;
    legacy_duplicate(impl_, &ret);
    return ret;
  }
  L* duplicate_locked() const {
    std::lock_guard<std::mutex> lk(mut);
    printf("in duplicate_locked\n");
    L* ret;
    legacy_duplicate(impl_, &ret);
    return ret;
  }
  mutable std::mutex mut;
};

int main() {
  A a(new L{1});
  const A b(new L{2});

  A c = a;
  A d = b;

  A e = a.copy_unsafe_for_multithreading();
  A f = const_cast<A&>(b).copy_unsafe_for_multithreading();

  printf("\npointers:\na=%p\nb=%p\nc=%p\nc=%p\nd=%p\nf=%p\n\n", a.impl_,
     b.impl_, c.impl_, d.impl_, e.impl_, f.impl_);

  printf("vals:\na=%d\nb=%d\nc=%d\nc=%d\nd=%d\nf=%d\n", a.impl_->val,
     b.impl_->val, c.impl_->val, d.impl_->val, e.impl_->val, f.impl_->val);
}

Вывод:

in duplicate_locked
in duplicate_locked
in duplicate
in duplicate

pointers:
a=0x7f85e8c01840
b=0x7f85e8c01850
c=0x7f85e8c01860
c=0x7f85e8c01870
d=0x7f85e8c01880
f=0x7f85e8c01890

vals:
a=1
b=2
c=1
c=2
d=1
f=2

Это следует Руководству по стилю Google , в котором const сообщает о безопасности потоков, но код, вызывающий ваш API, может отказаться, используя const_cast

...