За дизайном std::mutex
не є рухомим та копіюваним. Це означає, що клас, A
що містить мьютекс, не отримає конструктор переміщення за замовчуванням.
Як я можу зробити цей тип A
рухомим, захищеним від ниток?
За дизайном std::mutex
не є рухомим та копіюваним. Це означає, що клас, A
що містить мьютекс, не отримає конструктор переміщення за замовчуванням.
Як я можу зробити цей тип A
рухомим, захищеним від ниток?
std::lock_guard
метод is.
Відповіді:
Почнемо з трохи коду:
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. Наберіться терпіння, ми доїдемо.
Ваше запитання зводиться до:
Як написати конструктор переміщення та оператор присвоєння переміщення для цього класу?
Ми почнемо з конструктора переміщення.
Конструктор переміщення
Зверніть увагу, що учасник 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
first, а потім присвоювати їм значення лише після того, 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>;
Це може бути оптимізацією, але не точно. Вам доведеться виміряти, щоб визначити, чи є. Але за допомогою цієї зміни можна копіювати конструкцію з одного і того ж rhs в декількох потоках одночасно. Рішення 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 через проблему "блокування деталізації".
Також зверніть увагу на необхідність захисту від "самостійної заміни". "самозаміна" повинна бути забороною. Без самоперевірки один би рекурсивно зафіксував один і той же мьютекс. Це також можна вирішити без самоперевірки, використовуючи std::recursive_mutex
for 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();
}
На жаль, Якк стер свій коментар ще до того, як я отримав можливість завершити це оновлення. Але він заслуговує на честь за те, що просунув це питання та отримав рішення у цій відповіді.
Оновлення 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_)
{}
mutexes
в типи класів не є "єдиним істинним способом". Це інструмент у наборі інструментів, і якщо ви хочете ним скористатися, ось як.
Враховуючи, що, здається, не існує хорошого, чистого, простого способу відповісти на це - рішення Антона, я вважаю правильним, але воно, безумовно, спірне, якщо не з’явиться краща відповідь, я б рекомендував поставити такий клас на купу і доглядати через std::unique_ptr
:
auto a = std::make_unique<A>();
Зараз це повністю рухомий тип, і кожен, хто має блокування на внутрішньому мьютексі, поки відбувається переміщення, все ще безпечний, навіть якщо сперечається, чи це добре робити
Якщо вам потрібна семантика копіювання, просто використовуйте
auto a2 = std::make_shared<A>();
Це перевернута відповідь. Замість того, щоб вбудовувати "ці об'єкти потрібно синхронізувати" як основу типу, замість цього вводьте його під будь-який тип.
Ви поводитесь із синхронізованим об’єктом зовсім інакше. Велике питання полягає в тому, що ви повинні турбуватися про глухих кутах (блокуванні кількох об’єктів). По суті, це ніколи не повинно бути вашою "версією об'єкта за замовчуванням": синхронізовані об'єкти призначені для об'єктів, які будуть в суперечці, і вашою метою має бути мінімізація суперечок між потоками, а не підмітання їх під килимок.
Але синхронізація об’єктів все одно корисна. Замість того, щоб успадковувати синхронізатор, ми можемо написати клас, який обертає довільний тип у синхронізацію. Користувачам доводиться перебирати кілька обручів, щоб виконувати операції з об’єктом тепер, коли він синхронізований, але вони не обмежуються деяким обмеженим набором операцій над об’єктом, кодованим вручну. Вони можуть складати декілька операцій над об’єктом в одну або мати операції з кількома об’єктами.
Ось синхронізована обгортка навколо довільного типу T
:
template<class T>
struct synchronized {
template<class F>
auto read(F&& f) const&->std::result_of_t<F(T const&)> {
return access(std::forward<F>(f), *this);
}
template<class F>
auto read(F&& f) &&->std::result_of_t<F(T&&)> {
return access(std::forward<F>(f), std::move(*this));
}
template<class F>
auto write(F&& f)->std::result_of_t<F(T&)> {
return access(std::forward<F>(f), *this);
}
// uses `const` ness of Syncs to determine access:
template<class F, class... Syncs>
friend auto access( F&& f, Syncs&&... syncs )->
std::result_of_t< F(decltype(std::forward<Syncs>(syncs).t)...) >
{
return access2( std::index_sequence_for<Syncs...>{}, std::forward<F>(f), std::forward<Syncs>(syncs)... );
};
synchronized(synchronized const& o):t(o.read([](T const&o){return o;})){}
synchronized(synchronized && o):t(std::move(o).read([](T&&o){return std::move(o);})){}
// special member functions:
synchronized( T & o ):t(o) {}
synchronized( T const& o ):t(o) {}
synchronized( T && o ):t(std::move(o)) {}
synchronized( T const&& o ):t(std::move(o)) {}
synchronized& operator=(T const& o) {
write([&](T& t){
t=o;
});
return *this;
}
synchronized& operator=(T && o) {
write([&](T& t){
t=std::move(o);
});
return *this;
}
private:
template<class X, class S>
static auto smart_lock(S const& s) {
return std::shared_lock< std::shared_timed_mutex >(s.m, X{});
}
template<class X, class S>
static auto smart_lock(S& s) {
return std::unique_lock< std::shared_timed_mutex >(s.m, X{});
}
template<class L>
static void lock(L& lockable) {
lockable.lock();
}
template<class...Ls>
static void lock(Ls&... lockable) {
std::lock( lockable... );
}
template<size_t...Is, class F, class...Syncs>
friend auto access2( std::index_sequence<Is...>, F&&f, Syncs&&...syncs)->
std::result_of_t< F(decltype(std::forward<Syncs>(syncs).t)...) >
{
auto locks = std::make_tuple( smart_lock<std::defer_lock_t>(syncs)... );
lock( std::get<Is>(locks)... );
return std::forward<F>(f)(std::forward<Syncs>(syncs).t ...);
}
mutable std::shared_timed_mutex m;
T t;
};
template<class T>
synchronized< T > sync( T&& t ) {
return {std::forward<T>(t)};
}
Включені функції C ++ 14 та C ++ 1z.
це передбачає, що const
операції безпечні для кількох читачів (саме це std
передбачають контейнери).
Використання виглядає так:
synchronized<int> x = 7;
x.read([&](auto&& v){
std::cout << v << '\n';
});
для int
синхронізованого доступу.
Я б радив не мати synchronized(synchronized const&)
. Це рідко потрібно.
Якщо вам потрібно synchronized(synchronized const&)
, я мав би спокусу замінити T t;
на std::aligned_storage
, дозволивши ручне будівництво розміщення, і зробити ручне знищення. Це дозволяє належним чином керувати життям.
За винятком цього, ми могли б скопіювати джерело T
, а потім прочитати з нього:
synchronized(synchronized const& o):
t(o.read(
[](T const&o){return o;})
)
{}
synchronized(synchronized && o):
t(std::move(o).read(
[](T&&o){return std::move(o);})
)
{}
для призначення:
synchronized& operator=(synchronized const& o) {
access([](T& lhs, T const& rhs){
lhs = rhs;
}, *this, o);
return *this;
}
synchronized& operator=(synchronized && o) {
access([](T& lhs, T&& rhs){
lhs = std::move(rhs);
}, *this, std::move(o));
return *this;
}
friend void swap(synchronized& lhs, synchronized& rhs) {
access([](T& lhs, T& rhs){
using std::swap;
swap(lhs, rhs);
}, *this, o);
}
версії розміщення та вирівняного сховища трохи заплутаніші. Більшість доступу до t
буде замінено функцією-членом T&t()
і T const&t()const
, за винятком конструкції, де вам доведеться перестрибнути кілька обручів.
Роблячи synchronized
обгортку замість частини класу, все, що нам потрібно переконатись, це те, що клас внутрішньо поважає, const
як багатокористувацький, і записує його в одній нитці.
У рідкісних випадках нам потрібен синхронізований екземпляр, ми переходимо через обручі, як зазначено вище.
Вибачення за будь-які помилки в наведеному вище. Є, мабуть, такі.
Побічною перевагою вищезазначеного є те, що n-довільні довільні операції над synchronized
об’єктами (того самого типу) працюють разом, без необхідності жорсткого кодування перед цим. Додайте декларацію про друзів, і n-арні synchronized
об’єкти різних типів можуть працювати разом. Можливо, мені доведеться access
відмовитись від того, щоб бути вбудованим другом, щоб у такому разі мати справу з обмеженими перевантаженнями.
Використання мьютексів та семантики переміщення C ++ - це чудовий спосіб безпечної та ефективної передачі даних між потоками.
Уявіть собі «виробницьку» нитку, яка створює партії рядків і надає їх (одному або кільком) споживачам. Ці партії можуть бути представлені об’єктом, що містить (потенційно великі) std::vector<std::string>
об’єкти. Ми абсолютно хочемо "перемістити" внутрішній стан цих векторів до їх споживачів без зайвого дублювання.
Ви просто розпізнаєте мютекс як частину об'єкта, а не частину стану об'єкта. Тобто ви не хочете переміщати мьютекс.
Яке блокування вам потрібно, залежить від вашого алгоритму або того, наскільки узагальненими є ваші об’єкти, і який спектр використання ви дозволяєте.
Якщо ви тільки коли - або перейти від загального стану «виробника» об'єкта до локального потоку «споживаючи» об'єкт , який ви могли б бути в порядку , щоб тільки зафіксувати рухається від об'єкта.
Якщо це більш загальний дизайн, вам доведеться заблокувати обидва. У такому випадку вам потрібно буде врахувати глухий кут.
Якщо це є потенційною проблемою, скористайтеся std::lock()
для отримання блокування на обох мьютексах вільним способом.
http://en.cppreference.com/w/cpp/thread/lock
На завершення потрібно переконатись, що ви розумієте семантику переміщення. Нагадаємо, що переміщений об’єкт залишається у дійсному, але невідомому стані. Цілком можливо, що потік, який не виконує переміщення, має поважну причину для спроби отримати доступ до переміщеного з об’єкта, коли він може виявити цей дійсний, але невідомий стан.
Знову мій виробник просто вибиває струни, а споживач забирає все навантаження. У цьому випадку кожен раз, коли виробник намагається додати до вектора, він може виявити, що вектор не є порожнім або порожнім.
Коротше кажучи, якщо потенційний одночасний доступ до переміщеного з об'єкта становить запис, це, мабуть, все в порядку. Якщо це становить прочитане, то подумайте, чому нормально читати довільний стан.
Перш за все, у вашому дизайні має бути щось не так, якщо ви хочете перемістити об’єкт, що містить мьютекс.
Але якщо ви все-таки вирішите це зробити, вам доведеться створити новий мьютекс у конструкторі переміщення, наприклад:
// movable
struct B{};
class A {
B b;
std::mutex m;
public:
A(A&& a)
: b(std::move(a.b))
// m is default-initialized.
{
}
};
Це безпечно для потоку, оскільки конструктор переміщення може спокійно припустити, що його аргумент більше ніде не використовується, тому блокування аргументу не потрібно.
A a; A a2(std::move(a)); do some stuff with a
.
new
підняти екземпляр і помістити його в std::unique_ptr
- це здається чистішим і, швидше за все, не призведе до плутанини. Хороше питання.