Как бы просто ни был вопрос, понять это не так просто.Для начала мы можем поработать с простым конструктором копирования:
// almost pseudo code, mutex/lock/data types are synthetic
class test {
mutable mutex m;
data d;
public:
test( test const & rhs ) {
lock l(m); // Lock the rhs to avoid race conditions,
// no need to lock this object.
d = rhs.d; // perform the copy, data might be many members
}
};
Теперь создание оператора присваивания является более сложным.Первое, что приходит на ум, это просто делать то же самое, но в этом случае блокировать как lhs, так и rhs:
class test { // wrong
mutable mutex m;
data d;
public:
test( test const & );
test& operator=( test const & rhs ) {
lock l1( m );
lock l2( rhs.m );
d = rhs.d;
return *this;
}
};
Достаточно просто и неправильно.Хотя мы гарантируем однопоточный доступ к объектам (обоим) во время операции, и, таким образом, у нас нет условий гонки, у нас есть потенциальный тупик:
test a, b;
// thr1 // thr2
void foo() { void bar() {
a = b; b = a;
} }
И это не единственный потенциальный тупик,код не безопасен для самостоятельного назначения (большинство мьютексов не являются рекурсивными, и попытка заблокировать один и тот же мьютекс дважды заблокирует поток).Самая простая задача, которую нужно решить, - это самостоятельное назначение:
test& test::operator=( test const & rhs ) {
if ( this == &rhs ) return *this; // nothing to do
// same (invalid) code here
}
Для другой части проблемы вам необходимо установить порядок получения мьютексов.Это может быть обработано по-разному (сохранение уникального идентификатора для каждого объекта для сравнения ...)
test & test::operator=( test const & rhs ) {
mutex *first, *second;
if ( unique_id(*this) < unique_id(rhs ) {
first = &m;
second = &rhs.m;
} else {
first = &rhs.m;
second = &rhs.m;
}
lock l1( *first );
lock l2( *second );
d = rhs.d;
}
Конкретный порядок не так важен, как тот факт, что вам нужно обеспечить один и тот же порядок во всех случаях использования.или вы можете заблокировать потоки.Поскольку это довольно часто, некоторые библиотеки (включая предстоящий стандарт c ++) имеют специальную поддержку для него:
class test {
mutable std::mutex m;
data d;
public:
test( const test & );
test& operator=( test const & rhs ) {
if ( this == &rhs ) return *this; // avoid self deadlock
std::lock( m, rhs.m ); // acquire both mutexes or wait
std::lock_guard<std::mutex> l1( m, std::adopt_lock ); // use RAII to release locks
std::lock_guard<std::mutex> l2( rhs.m, std::adopt_lock );
d = rhs.d;
return *this;
}
};
Функция std::lock
получит все блокировки, переданные в качестве аргумента, и гарантирует, что порядокполучение является тем же, гарантируя, что если весь код, который должен получить эти два мьютекса, сделает это с помощью std::lock
, то не будет тупиков.(Вы все еще можете заблокировать, вручную заблокировав их где-то отдельно)Следующие две строки хранят блокировки в объектах, реализующих RAII, так что, если операция присваивания завершается неудачей (исключение выдается), блокировки снимаются.
Это можно записать по-разному, используя std::unique_lock
вместо std::lock_guard
:
std::unique_lock<std::mutex> l1( m, std::defer_lock ); // store in RAII, but do not lock
std::unique_lock<std::mutex> l2( rhs.m, std::defer_lock );
std::lock( l1, l2 ); // acquire the locks
Я просто подумал о другом, гораздо более простом подходе, который я здесь зарисовываю.Семантика немного отличается, но может быть достаточной для многих приложений:
test& test::operator=( test copy ) // pass by value!
{
lock l(m);
swap( d, copy.d ); // swap is not thread safe
return *this;
}
}
Существует семантическое различие в обоих подходах, поскольку тот, который имеет идиому копирования и замены, имеетпотенциальное состояние гонки (которое может или не может повлиять на ваше приложение, но вы должны знать об этом).Поскольку обе блокировки никогда не удерживаются одновременно, объекты могут меняться в промежутке времени, когда первая блокировка снята (копия аргумента завершена), а вторая блокировка получена внутри operator=
.
. Например, какэто может не сработать, примите во внимание, что data
является целым числом и что у вас есть два объекта, инициализированные одним и тем же целочисленным значениемОдин поток получает обе блокировки и увеличивает значения, в то время как другой поток копирует один из объектов в другой:
test a(0), b(0); // ommited constructor that initializes the ints to the value
// Thr1
void loop() { // [1]
while (true) {
std::unique_lock<std::mutex> la( a.m, std::defer_lock );
std::unique_lock<std::mutex> lb( b.m, std::defer_lock );
std::lock( la, lb );
++a.d;
++b.d;
}
}
// Thr1
void loop2() {
while (true) {
a = b; // [2]
}
}
// [1] for the sake of simplicity, assume that this is a friend
// and has access to members
С реализациями operator=
, которые выполняют одновременные блокировки для обоих объектов, вы можете утверждать вв любой момент времени (безопасное выполнение потоков путем получения обеих блокировок), что a
и b
одинаковы, что, как кажется, ожидается при быстром чтении кода.Это не выполняется, если operator=
реализован в терминах идиомы копирования и обмена.Проблема в том, что в строке, помеченной как [2], b
заблокирована и скопирована во временную, а затем разблокировка снята.Затем первый поток может получить обе блокировки одновременно и увеличить значения a
и b
до того, как a
будет заблокирован вторым потоком в [2].Затем a
перезаписывается значением, которое b
имело до приращения.