Скопіюйте конструктор з аргументом non-const, запропонованим правилами безпеки потоку?


9

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

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-функцією, поведінка лише задокументована, але не має поняття constness.)

Я можу придумати багато можливих сценаріїв:

(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) або навіть просто оголошувати duplicateconst і брехати про безпеку ниток у всіх контекстах. (Зрештою, про застарілу функцію це не хвилює, constтому компілятор навіть не скаржиться.)

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

(4) Нарешті, я можу слідувати логіці та зробити конструктор копій, який бере аргумент non-const .

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) або до комбінації обох.


EDIT 1:

Ще один варіант:

(6) Забудьте про все непочуття функції дублюючого члена та просто зателефонуйте legacy_duplicateз конструктора та заявіть, що конструктор копіювання не є безпечним для потоку. (І за необхідності зробіть інший безпечний потік версії типу, A_mt)

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

EDIT 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; 
}

EDIT 3: Нещодавно я дізнався, що у мене std::auto_ptrбула схожа проблема з конструктором "копіювати", який не має const. Ефект полягав у тому, що auto_ptrйого не можна було використовувати у контейнері. https://www.quantstart.com/articles/STL-Containers-and-Auto_ptrs-Why-They-Dont-Mix/


1
" У цьому застарілому коді функція, що дублює об'єкт, не є безпечною для потоків (при виклику того ж першого аргументу) " Ви впевнені в цьому? Чи є якийсь стан, не міститься в Lякому, змінюється шляхом створення нового Lпримірника? Якщо ні, то чому ви вважаєте, що ця операція не є безпечною для потоків?
Нікол Болас

Так, така ситуація. Схоже, внутрішній стан першого аргументу змінюється під час виконання. Чомусь (деяка "оптимізація" або поганий дизайн або просто за специфікацією) функцію legacy_duplicateнеможливо викликати одним і тим же першим аргументом з двох різних потоків.
alfC

@TedLyngmo добре, що я зробив. Хоча технічно в c ++ до 11 const є більш нечітке значення за наявності ниток.
alfC

@TedLyngmo так, це досить гарне відео. Шкода, що відео просто стосується належних членів і не зачіпає проблеми будівництва (якщо також була жорсткість на "іншому" об'єкті). В перспективі не може бути ніякого внутрішнього способу зробити цю нитку обгортки безпечною при копіюванні без додавання іншого шару абстрагування (та бетонної яхти).
alfC

Так, це мене збентежило, і я, мабуть, один із тих людей, хто не знає, що constнасправді означає. :-) Я б не замислювався над тим, щоб взяти const&у себе копію копію, доки я не зміню other. Я завжди думаю про безпеку ниток як про щось, що додається до того, що потрібно отримати з декількох потоків, за допомогою інкапсуляції, і я дуже чекаю на відповіді.
Тед Лінгмо

Відповіді:


0

Я б просто включив обидва Ваші параметри (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


Дякую за відповідь, я думаю, що це не змінить вашу відповідь, і я не впевнений, але кращою моделлю для цього 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; }(тобто non-const in)
alfC

Ваша відповідь дуже цікава, оскільки вона може поєднуватися з варіантом (4) та явною версією варіанту (2). Тобто, A a2(a1)можна намагатися бути безпечними для потоків (або бути видаленими) і A a2(const_cast<A&>(a1))взагалі не намагатимуться бути безпечними для потоку.
alfC

2
Так, якщо ви плануєте використовувати Aяк безпечний для потоків, так і безпечний для потоків контекст, слід підтягнути const_castдо викликового коду, щоб було зрозуміло, де, як відомо, порушено безпеку потоку. Добре просувати додаткову безпеку за API (mutex), але не добре приховувати небезпеку (const_cast).
Михайло Грачик

0

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

Думаю, ключовим моментом, який слід помітити, є те, що ви додаєте функцію, яка раніше не існувала: можливість копіювання об'єкта з декількох потоків одночасно.

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

Тому будь-яке використання цієї нової функції стане вашим кодом, а не успадковано як існуючий функціонал. Ви повинні бути тим, хто знає, чи дійсно додавання додаткового блокування буде дорогим - залежно від того, як часто ви будете використовувати цю нову функцію.

Також, виходячи із сприйнятої складності об'єкта - за спеціальним поводженням, який ви йому надаєте, я припускаю, що процедура дублювання не є тривіальною, отже, вже досить дорогою з точки зору продуктивності.

Виходячи з вищезазначеного, у вас є два шляхи, якими ви можете слідувати:

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

B) Ви підозрюєте, що копіювання з декількох потоків траплятиметься досить часто, щоб зайве блокування було проблемою. Тоді у вас є лише один варіант - виправити код дублювання. Якщо ви не виправите це, вам все одно потрібно буде заблокувати, чи то на цьому шарі абстракції, чи десь ще, але він вам знадобиться, якщо ви не хочете помилок - і, як ми встановили, на цьому шляху ви припускаєте це блокування буде занадто дорогим, тому єдиним варіантом є фіксація коду дублювання.

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

Теоретично існує інша ситуація:

C) На відміну від уявної складності функції дублювання, вона насправді є тривіальною, але з певних причин її неможливо виправити; це настільки банально, що навіть безперечний спіклок вносить неприйнятну деградацію продуктивності до дублювання; дублювання на паралельних нитках використовується рідко; дублювання на одному потоці використовується весь час, що робить погіршення продуктивності абсолютно неприйнятним.

У цьому випадку я пропоную наступне: оголосити конструкторів / операторів копіювання за замовчуванням видалені, щоб хтось не випадково їх не використав. Створіть два способи дублювання, які явно можна викликати, безпечний потік та небезпечний потік; змушуйте своїх користувачів називати їх явно, залежно від контексту. Знову ж таки, немає іншого способу досягти прийнятної продуктивності однієї нитки та безпечної багатопотокової нитки, якщо ви дійсно в цій ситуації і просто не можете виправити існуючу реалізацію дублювання. Але я вважаю, що це насправді малоймовірно.

Просто додайте цей мутекс / спінлок та орієнтир.


Чи можете ви вказати мені матеріал про спінлок / попередньо спінінг mutex у C ++? Чи є щось складніше, ніж те, що надається std::mutex? Функція дублювання не є секретом, я не згадував її, щоб підтримувати проблему на високому рівні та не отримувати відповіді про MPI. Але оскільки ви пройшли так глибоко, я можу дати вам більше деталей. Спадкова функція є MPI_Comm_dupі тут описана ефективна безпека без потоків (я це підтвердив) github.com/pmodels/mpich/isissue/3234 . Ось чому я не можу виправити дублікат. (Крім того, якщо я додаю mutex, я буду спокушатися зробити всі виклики MPI потоками безпечними.)
alfC

На жаль, я не знаю багато std :: mutex, але я думаю, що це робить деяке обертання, перш ніж давати процес спати. Добре відомим пристроєм синхронізації, на якому ви можете керувати цим вручну, є: docs.microsoft.com/en-us/windows/win32/api/synchapi/… Я не порівнював продуктивність, але здається, що std :: mutex є тепер вищий: stackoverflow.com/questions/9997473/… та реалізований за допомогою: docs.microsoft.com/en-us/windows/win32/sync/…
DeducibleSteak

Здається , це хороший опис загальних міркувань , щоб прийняти до уваги: stackoverflow.com/questions/5869825 / ...
DeducibleSteak

Ще раз дякую, я в Linux, якщо це має значення.
alfC

Ось дещо детальне порівняння продуктивності (для іншої мови, але, мабуть, це інформативно і вказує на те, чого очікувати): matklad.github.io/2020/01/04/… TLDR є - спінлок виграє надзвичайно мало маржа, коли немає суперечок, може втратити сильно, коли є суперечка.
DeducibleSteak
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.